Closed5
『【初学者向け】具体例で学ぶTypeScript練習問題集』を解く
本スクラップについて
『【初学者向け】具体例で学ぶTypeScript練習問題集』を解いていき、調べたことや疑問点をメモしていきます。
注意事項
- 一目見て解ける問題については記載しない
- 問題・答えは上記サイトを参照すること(本スクラップでは基本的に転記しない)
第 1 章 JS から TS へ ~型注釈~
【Lv.2】引数の型注釈 5(関数)
「数値を引数に取り、返り値のない関数」 を引数として受け取る関数
解答
const call = (func: (num:number) => void) => {
// 略
};
【Lv.2】引数の型注釈 6(組み込みオブジェクト)
日付オブジェクトを受け取り、その日が平日か休日かを判定する関数
解答
const isHoliday = (date: Date) => {
// 略
};
参考
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) => {
// 略
};
- 参考
【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
と頭文字が小文字だったため、エラーにさえならず。。
- ちなみに今回の私の解答は
参考
【Lv.3】オブジェクトのインデックス
解答
- まず以下を試した
- エラーは出なくなった
- ただし、valueがstringであればなんでも良くなってしまう。。
type winType = {
[K:string] :string
}
const win: winType = {
gu: "pa",
choki: "gu",
pa: "choki",
};
valueをwinのvalueのみにしたくて調べていたら以下の記事を発見。
試してみようと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",
}
参考
第 2 章 JS から TS へ ~型ガード~
【Lv.1】型ガード 1(find)
-
.find()
で見つかった要素に対して処理を行うコード - 一致する要素がなく
undefined
になってもエラーにならないようにする
解答
- オプショナルプロパティにする
console.log(result?.name);
答え
- オプショナルプロパティじゃなくてオプショナルチェーン
- オプションプロパティは、必須ではないオブジェクトのプロパティ
参考
別解
- 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にしない方が良いと思う
- 取得した要素がHTMLButtonElementであるとは限らないから
document.querySelectorAll(".special-button").forEach((button) => {
(button as HTMLButtonElement).disabled = true;
});
参考
解答例
例1 instanceofで型を絞り込む
- typeofとinstanceofを混同しがち、、
- typeofは型を調べる
- instanceofはオブジェクトがそのクラスのインスタンスかどうか調べる
if (button instanceof HTMLButtonElement) {
button.disabled = true;
}
例2 disabledを持つかチェックする
if ("disabled" in button) {
button.disabled = true;
}
参考
in 演算子は、指定されたプロパティが指定されたオブジェクトにある場合に true を返します。
例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);
}
参考
-
zod
はバリデーションで使えるライブラリ
別解
- typeofやinで型を絞り込む
- やや冗長
if (typeof e === "object" && e !== null && "message" in e) {
console.log(e.message);
}
第 3 章 より安全な型へ
【Lv.2】テンプレートリテラル型 1
- 年/月/日(スラッシュ区切り)の形式の文字列のみ に制限
解答
- 正規表現を使う?など色々悩んでしまった
答え
const dateList: `${number}/${number}/${number}`[] = [
// 略
];
参考
【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 の配列のみを受け入れるように型注釈を指定したい
調べた
-
TypeScriptで最低n個の要素を持った配列の型を宣言する方法
- 古い記事なので今ではもう少しやりようがありそう
-
ts-array-lengthを支えるテクニック
- 配列の長さをチェックする用のライブラリまである、、
- つまりライブラリを入れないとできないレベルのこと??
- 配列の長さをチェックする用のライブラリまである、、
答え
- めっちゃシンプルに解ける
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 文で全てのケースを網羅しているかをチェックして、抜けている場合はエラーを返す
調べた
-
TypeScriptのexhaustiveness checkをスマートに書く
- よく理解できなかった、、
答え
- 「どんな値も入らない」ことを表す 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");
参考
【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());
}
参考
感想
- 巷の問題集は難しいものが多いので、こちらはサバイバルTypeScriptを終えたくらいの初学者が取り組むのにはちょうど良かった
- ジェネリクスの問題がないのが残念だった
- 絶賛苦戦中なので、、
このスクラップは2024/02/17にクローズされました