Closed5

『【初学者向け】具体例で学ぶTypeScript練習問題集』を解く

wsigma21wsigma21

本スクラップについて

『【初学者向け】具体例で学ぶTypeScript練習問題集』を解いていき、調べたことや疑問点をメモしていきます。

https://zenn.dev/kagan/articles/typescript-practice

注意事項

  • 一目見て解ける問題については記載しない
  • 問題・答えは上記サイトを参照すること(本スクラップでは基本的に転記しない)
wsigma21wsigma21

第 1 章 JS から TS へ ~型注釈~

【Lv.2】引数の型注釈 5(関数)

「数値を引数に取り、返り値のない関数」 を引数として受け取る関数

解答

const call = (func: (num:number) => void) => {
  // 略
};

【Lv.2】引数の型注釈 6(組み込みオブジェクト)

日付オブジェクトを受け取り、その日が平日か休日かを判定する関数

解答

const isHoliday = (date: Date) => {
  // 略
};

参考

Date型が存在する

https://typescriptbook.jp/reference/builtin-api/date

【Lv.2】引数の型注釈 7(イベントリスナー)

HTML 要素に keydown のイベントリスナー を追加する

解答

  • 以下だとType 'KeyboardEvent' is not generic.(2315)エラーになる
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
  // ✅ 想定通りのコード。エラーにならないようにしてください
  if (e.key === "Enter") {
    console.log("Enter キーが押されました");
  }
};
  • 単純にKeyboardEventオブジェクトで良い
const handleKeyDown = (e: KeyboardEvent) => {
  // 略
};
  • 参考

https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent

【Lv.3】window を拡張する

window オブジェクトに dataLayer という配列の型定義を追加

解答

// ✍🏼 型定義を追加してください
// 以下は誤ったコード
type window = {
  dataLayer: [{}]
}

// ✅ 想定通りのコード。エラーにならないようにしてください
window.dataLayer.push({ event: "event_name" });
window.dataLayer.push({ variable_name: "variable_value" });
window.dataLayer.push({
  color: "red",
  conversionValue: 50,
  event: "customize",
});
window.location; // 既存のプロパティにもアクセスできる
window.scrollY; // 既存のプロパティにもアクセスできる

// ❌ 以下はエラーにしてください
window.dataLayer.toUpperCase(); // 存在しないプロパティにアクセスするとエラーにしたい
window.bar; // 存在しないプロパティにアクセスするとエラーにしたい

答え

  • interfaceなら既存の型に新しいオブジェクトを追加できる
interface Window {
  dataLayer: Object[]
}
  • 同名のものを宣言したときのinterfaceとtype(型エイリアス)の違い
    • interface:マージされる
    • type:エラーになる
      • ちなみに今回の私の解答はwindowと頭文字が小文字だったため、エラーにさえならず。。

参考

https://typescriptbook.jp/reference/object-oriented/interface/interface-vs-type-alias

https://zenn.dev/luvmini511/articles/6c6f69481c2d17

【Lv.3】オブジェクトのインデックス

解答

  • まず以下を試した
    • エラーは出なくなった
    • ただし、valueがstringであればなんでも良くなってしまう。。
type winType = {
  [K:string] :string
}
const win: winType = {
  gu: "pa",
  choki: "gu",
  pa: "choki",
};

valueをwinのvalueのみにしたくて調べていたら以下の記事を発見。

https://zenn.dev/kata_n/articles/099ec8fe6322d0

試してみようとas constを付けたら、これだけで上手くいったっぽい。

const win = {
  gu: "pa",
  choki: "gu",
  pa: "choki",
} as const;

なぜ??

答え

Widening と呼ばれる型推論の仕組みによって、win は { gu: string; choki: string; pa: string; } と推論されてしまいます。
そのため、値を string ではなく、具体的な文字列リテラル型にする必要があります

as constはオブジェクトのプロパティを再帰的にreadonlyにするために使うもの、という認識だったけど、Wideningを防ぐためにも使えるようだ。

別解

型注釈を使う。
こちらの方が私がやりたかったことに近い。
Recordで指定したkey, valueを持つオブジェクトの型を作れる。

type Hand = "pa" | "gu" | "choki";

const win: Record<Hand, Hand> = {
  gu: "pa",
  choki: "gu",
  pa: "choki",
} 

参考

https://typescriptbook.jp/reference/values-types-variables/object/index-signature

https://typescriptbook.jp/reference/values-types-variables/const-assertion

https://nonakayasuo.com/as-const/

https://typescriptbook.jp/reference/type-reuse/utility-types/record

wsigma21wsigma21

第 2 章 JS から TS へ ~型ガード~

【Lv.1】型ガード 1(find)

  • .find() で見つかった要素に対して処理を行うコード
  • 一致する要素がなくundefinedになってもエラーにならないようにする

解答

  • オプショナルプロパティにする
console.log(result?.name);

答え

  • オプショナルプロパティじゃなくてオプショナルチェーン
    • オプションプロパティは、必須ではないオブジェクトのプロパティ

参考

https://typescriptbook.jp/reference/values-types-variables/object/optional-chaining

別解

  • if文を使って型を絞り込む
const result = items.find((item) => item.id === 1);
if (result) {
  console.log(result.name);
}

【Lv.2】型ガード 3(querySelectorAll)

  • 特定のクラスを持つ button 要素に対して disabled を設定する
  • querySelectorAll で要素を取得するとひとつひとつの要素は Element 型
    • div, aなど他の要素が含まれる可能性があるため
  • disabledを設定するとエラーになる
    • Property 'disabled' does not exist on type 'Element'.
  • エラーにならないようにする

解答

  • buttonはHTMLButtonElement
  • HTMLButtonElementで型アサーションすることで上書きする
  • この方法は非推奨
    • 取得した要素がHTMLButtonElementであるとは限らないから
      • 個人的には、それならforEachのcallbackの引数名をbuttonにしない方が良いと思う
document.querySelectorAll(".special-button").forEach((button) => {
  (button as HTMLButtonElement).disabled = true;
});

参考

https://typescriptbook.jp/reference/values-types-variables/type-assertion-as

解答例

例1 instanceofで型を絞り込む

  • typeofとinstanceofを混同しがち、、
    • typeofは型を調べる
    • instanceofはオブジェクトがそのクラスのインスタンスかどうか調べる
  if (button instanceof HTMLButtonElement) {
    button.disabled = true;
  }

例2 disabledを持つかチェックする

  if ("disabled" in button) {
    button.disabled = true;
  }
参考

in 演算子は、指定されたプロパティが指定されたオブジェクトにある場合に true を返します。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/in

例3 予めquerySelectorAllでbuttonに絞り込む

  • buttonが多いとforEachの処理が重くなる可能性がある
  • 個人的には、forEachのcallbackの引数名をbuttonにしても良いのはこういうケースだと思う
document
  .querySelectorAll("button")
  .forEach((button) => {
    if(button.matches(".special-button")) {
      button.disabled;
    }
});

【Lv.2】型ガード 4(エラー)

  • tsconfig.json で strict: true を設定している場合、キャッチしたエラーオブジェクトの型は(any ではなく)unknown になる
    • エラーオブジェクトのプロパティにアクセスするとエラーになってしまう

解答

  • eはErrorオブジェクトだから、instanceofで型を絞り込めそう
if (e instanceof Error) {
    console.log(e.message);
  }

参考

https://typescriptbook.jp/reference/statements/unknown

  • zodはバリデーションで使えるライブラリ

別解

  • typeofやinで型を絞り込む
    • やや冗長
if (typeof e === "object" && e !== null && "message" in e) {
    console.log(e.message);
  }
wsigma21wsigma21

第 3 章 より安全な型へ

【Lv.2】テンプレートリテラル型 1

  • 年/月/日(スラッシュ区切り)の形式の文字列のみ に制限

解答

  • 正規表現を使う?など色々悩んでしまった

答え

const dateList: `${number}/${number}/${number}`[] = [
  // 略
];

参考

https://engineering.meetsmore.com/entry/2022/12/06/215335

【Lv.2】テンプレートリテラル型 2

  • href は / から始まる文字列か、http:// または https:// から始まる文字列 に制限

解答

  • 1つ前の問題と同じように解ける
type NavItem = {
  label: string;
  href: `/${string}` | `http://${string}` | `https://${string}`; 
}; 

答え

  • より洗練された書き方、、
type NavItem = {
  label: string;
  href: `/${string}` | `${"http" | "https"}://${string}`;
}; 

【Lv.2】要素数の決まった配列 1

  • ポケモンの情報を持つオブジェクト
  • 要素数が 1 または 2 の配列のみを受け入れるように型注釈を指定したい

調べた

答え

  • めっちゃシンプルに解ける
type Pokemon = {
  name: string;
  type: [string] | [string, string]
};

【Lv.3】要素数の決まった配列 2

  • 配列の先頭の要素の先頭の文字を取得する関数
  • 引数の型を 「要素数が 1 以上の配列」 に制限したい

答え

  • 1個前の問題の応用
const headOfHead = (array: [string, ...string[]]) => {
  return array[0].charAt(0);
};

【Lv.3】判別可能なユニオン型 1

  • 図形の形と大きさを表すオブジェクトを受け取り、その図形の面積を返す関数
  • type が "circle"(円)の場合は radius(半径)が必須、type が "square"(正方形)の場合は side(辺の長さ)が必須 になるように型を修正

解答

type Shape = 
  | {type: "circle"; radius: number}
  | {type: "square"; side: number};

【Lv.3】switch 文の網羅性チェック(exhaustiveness check)

  • 信号の色を表す文字列を受け取り、それに応じて行動を返す関数
  • switch 文で全てのケースを網羅しているかをチェックして、抜けている場合はエラーを返す

調べた

答え

  • 「どんな値も入らない」ことを表す never 型を使うことで、switch 文の網羅性をチェックできる
  • 網羅されていれば、default ブロックでは light が never 型になるはず
  • satisfiesは TS 4.9 で追加された比較的新しい機能
    • 変数がその型を満たすかチェックできる
const action = (light: "RED" | "YELLOW" | "GREEN") => {
  switch (light) {
    case "RED": {
      console.log("止まれ");
      break;
    }
    case "GREEN": {
      console.log("進め");
      break;
    }
    default: {
      // 
      light satisfies never;
      // break;
    }
  }
};

action("RED");
action("GREEN");
action("YELLOW");

参考

https://qiita.com/chelproc/items/c0017cfa268409d2748b

【Lv.3】不明な返り値の型ガード

  • JSON 文字列をパースして、パースしたオブジェクトの特定のプロパティにアクセスするコード
  • JSON.parse() は any 型を返しますが、本来 unknown 型を返すべき
    • どんな値になるか、実行するまでわからないため
  • any にすればコンパイルエラーは回避できますが、実行時にエラーが発生してしまう
  • 値の型を絞り込み、エラーを回避

解答

  • 以下ではダメ
    • 'parsed' is of type 'unknown'.(18046)
if ('name' in parsed) {
    console.log(parsed.name.toUpperCase());
  }
  • unknownに対してin演算子を使うには色々工夫がいる、、
  if (
    typeof parsed === "object" &&
    parsed !== null &&
    "name" in parsed &&
    typeof parsed.name === "string"
  ) {
    console.log(parsed.name.toUpperCase());
  }

参考

https://azukiazusa.dev/blog/typescript-4-9-in/

wsigma21wsigma21

感想

  • 巷の問題集は難しいものが多いので、こちらはサバイバルTypeScriptを終えたくらいの初学者が取り組むのにはちょうど良かった
  • ジェネリクスの問題がないのが残念だった
    • 絶賛苦戦中なので、、
このスクラップは2024/02/17にクローズされました