Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TypeScript type challengesを解いてみよう

TypeScript type challengesを解いてみよう

Nakano as a Service

October 21, 2023
Tweet

More Decks by Nakano as a Service

Other Decks in Technology

Transcript

  1. 流れ 1. TypeScript の成り立ち 2. TypeScript の型の基本 3. TypeScript type

    challenges 初級編を解く 1. ユーティリティ型の解説 2. 解答解説
  2. TypeScript の成り立ち 予期せぬ動作を起こす 実行するまで正常に動作するかわからない バグの温床 JavaScript には型が無い function add(a, b)

    { return a + b; } add(5, "10"); // 出力: "510" add(5, [1, 2]); // 出力: "51,2" add(5, null); // 出力: 5 add(5, undefined); // 出力: NaN
  3. TypeScript の成り立ち コンパイル時に型をチェックし、型の部分を消し去る(トランスパイルと言う) 既存のJS コード資産に型を付け加えられる → それだけの柔軟性がある JavaScript の文法はそのままに型を書けるようにした function

    add(a: number, b: number): number { return a + b; } add(5, "10"); // エラー: 引数 'b' の型 '"10"' はパラメーター型 'number' に割り当てられません。 add(5, [1, 2]); // エラー: 引数 'b' の型 'number[]' はパラメーター型 'number' に割り当てられません。 add(5, null); // エラー: 引数 'b' の型 'null' はパラメーター型 'number' に割り当てられません。 add(5, undefined); // エラー: 引数 'b' の型 'undefined' はパラメーター型 'number' に割り当てられません。 function add(a, b) { return a + b; }
  4. 柔軟性の入り口: ジェネリック型 この <...> の部分を型引数という ジェネリック型の型引数 → 関数でいう引数 ジェネリック型は関数の型バージョン 型を引数にとり型を返す関数

    type Box<T> = { content: T } type NumberBox = Box<number> type StringBox = Box<string> type DateBox = Box<Date> type BooleanBox = Box<boolean> // 使用例 const bbox: BooleanBox = { content: true } const bbox: Box<boolean> = { content: true } // 直接書くこともできる ` `
  5. extends による型制約 これで Box<T> の T には number しか入らなくなった。 →

    〜身体は強い型を求める〜 ` ` type Box<T extends number> = { content: T } type NumberBox = Box<number> type StringBox = Box<string> // 型エラー type DateBox = Box<Date> // 型エラー type BooleanBox = Box<boolean> // 型エラー ` ` ` ` ` `
  6. リテラル型とユニオン型 これを用いて 3 か "four" しか入らないBox 型を作れる type Three =

    3 // 3 しか入らない型(リテラル型) type Four = 4 // 4 しか入らない型(リテラル型) type ThreeOrFour = Three | Four // 3 または4 しか入らない型(ユニオン型) const x: ThreeOrFour = 3 const y: ThreeOrFour = 4 const z: ThreeOrFour = 334 // 型エラー ` ` ` ` type ThreeOrFourBox<T extends 3 | "four"> = { content: T } type FourBox = ThreeOrFourBox<"four"> type FourBox = ThreeOrFourBox<5> // 型エラー const x: FourBox = { content: "four" } const x: FourBox = { content: 4 } // 型エラー
  7. ここまでのまとめ TypeScript はJavaScript に型を後付けする言語 ジェネリック型 <...> は型を引数に取り型を返す関数 関数の引数に型をつけるように、ジェネリック型の型引数に型制約をつけた リテラル型で 3

    や "foo" しか入らない型を作った ユニオン型 | で 3 または "foo" しか入らない型を作った あとは実践あるのみ TypeScript type challengesを解いてみよう! ※ 問題文は簡単のために一部改変されています ` ` ` ` ` ` ` ` ` ` ` `
  8. 初級1-Pick: 問題 例えば: 組み込みの型ユーティリティ Pick<T, K> を使用せず、 T から K

    のプロパティを抽出する型を実装します。 ` ` ` ` ` ` type Todo = { title: string description: string completed: boolean } type TodoPreview = MyPick<Todo, 'title' | 'completed'> const todo: TodoPreview = { title: 'Clean room', completed: false, }
  9. 初級1-Pick: 解説 T extends object 型引数 T は object 型であり、

    string 型や number 型などではないという型制約 keyof T : keyof 型演算子 まず型引数の制約から考える type MyPick<T extends object, K extends keyof T> = { /* あとで書く */ } ` ` ` ` ` ` ` ` ` ` ` ` type Book = { title: string; price: number; rating: number; }; type BookKey = keyof Book; // 上は次と同じ意味になる type BookKey = "title" | "price" | "rating";
  10. 初級1-Pick: 解説 { [X in K]: X } : Mapped

    Types 型変数 K に与えられたユニオン型をfor 文のように型変数 X に割り当て、 X をキーとするオブジェクト型を 返す。またそのキー X に対応する : の右辺で X が使える。 そして K をオブジェクト型のキーとすることを考える ` ` type MyPick<T extends object, K extends keyof T> = { [X in K]: X /* まだT を使っていない */ } ` ` ` ` ` ` ` ` ` ` ` ` ` ` type AB = { [X in "a" | "b"]: X } // これは以下と同義 type AB = { a: "a", b: "b" }
  11. 初級1-Pick: 解説 T[X] : Indexed Access Types オブジェクト型や配列型に対して X 型でインデックス参照した際に、返しうるプロパティや要素の型を返す。

    完成形 type MyPick<T extends object, K extends keyof T> = { [X in K]: T[X] } ` ` ` ` type Todo = { // オブジェクト型の例 title: string completed: boolean } type TitleOfTodo = Todo["title"] // string になる type TitleOfTodo = Todo[string] // string | boolean になる type Arr = boolean[] // 配列型の例 type TypeOfArr = Arr[334] // boolean になる type TypeOfArr = Arr[number] // 同じくboolean になる
  12. 初級1-Pick: 解説 例: T に {a:number, b:string, c:boolean} 型、 K

    に "a" | "c" 型を割り当てた時 完成形 type MyPick<T extends object, K extends keyof T> = { [X in K]: T[X] } ` ` ` ` ` ` ` ` // T だけを割り当てた MyPick<{ a: number, b: string, c: boolean }, K extends "a" | "b" | "c"> // K も割り当てた MyPick<{ a: number, b: string, c: boolean }, "a" | "c"> => { [X in "a" | "c"]: { a: number, b: string, c: boolean }[X] } => { "a": { a: number, b: string, c: boolean }["a"], "c": { a: number, b: string, c: boolean }["c"] } => { a: number, c: boolean }
  13. 初級2-Readonly: 問題 例えば: 組み込みの型ユーティリティ Readonly<T> を使用せず、 T のすべてのプロパティを読み取り専用にする型を 実装します。実装された型のプロパティは再割り当てできません。 `

    ` ` ` type Todo = { title: string description: string } const todo: MyReadonly<Todo> = { title: "Hey", description: "foobar" } todo.title = "Hello" // Error: cannot reassign a readonly property todo.description = "barFoo" // Error: cannot reassign a readonly property
  14. 初級2-Readonly: 解説 MappedTypes にはreadonly 属性をつけられる。 例: T に { x:

    number, y: string } 型を与えたときの型演算の途中経過 完成形 type MyReadonly<T extends object> = { readonly [X in keyof T]: T[X] } ` ` ` ` MyReadonly<{ x : number, y: string }> => { readonly [X in "x" | "y"]: { x: number, y: string }[X] } => { readonly x: { x: number, y: string }["x"], readonly y: { x: number, y: string }["y"] } => { readonly x: number, readonly y: string }
  15. 初級3-Tuple to Object: 問題 例えば: タプルを受け取り、その各値のkey/value を持つオブジェクトの型に変換する型を実装します。 type Tuple =

    ['tesla', 'model 3', 'model X', 'model Y'] type Result = TupleToObject<Tuple> const r: Result = { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y' }
  16. 初級3-Tuple to Object: 解説 要素数と各要素の型が決まっている配列型 ただの配列型の例: number[] , string[] ,

    "a"[] , 1[] タプル型の例: [number, string] 、 ["a", 1, true] 配列型をさらに厳しくしたもの: [number, string] extends any[] リテラル表記の配列の型がタプル型だと主張したい時は [...] as const と書く そもそもタプル型とは ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` type T = ['tesla', 'model 3', 'model X', 'model Y'] // タプル型の宣言 const teslaArr: string[] = ['tesla', 'model 3', 'model X', 'model Y'] const teslaTuple: T = ['tesla', 'model 3', 'model X', 'model Y'] // これはエラー(リテラル表記の配列) const teslaTuple: T = ['tesla', 'model 3', 'model X', 'model Y'] as const // タプルであることを主張
  17. 初級3-Tuple to Object: 解説 T[number] : タプル T に対するIndexed Access

    Types タプル型は配列型と異なり各要素の型が決まっているので、その型を返す 例: T が ["a", 1, true] → T[2] は true 型、 T[number] は "a" | 1 | true 型 例: T が string[] → T[100] や T[number] も string 型 完成形 type TupleToObject<T extends any[]> = { [X in T[number]]: X } ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` TupleToObject<["a", "b", "c"]> = { [X in ["a", "b", "c"][number]]: X } => { [X in "a" | "b" | "c"]: X } => { a: "a", b: "b", c: "c" }
  18. 初級4-First of Array: 問題 例えば: 配列 T を受け取り、その最初のプロパティの型を返す First<T> を実装します。

    ` ` ` ` type arr1 = ['a', 'b', 'c'] type arr2 = [3, 2, 1] type head1 = First<arr1> // 'a' になる type head2 = First<arr2> // 3 になる type head3 = First<[]> // never になる
  19. 初級4-First of Array: 解説 Indexed Access Types を使って配列(またはタプル)の0 番目の型を返せばいいはず 今回は

    never 型が求められているので never 型を返したい ちなみに実際にインデックス外にアクセスすると 惜しい回答 type First<T extends any[]> = T[0] type head3 = First<[]> // never 型であってほしいがundefined 型になってしまう! ` ` ` ` const emptyArr: [] = [] as const console.log(emptyArr[0]) // undefined が出力される
  20. 初級4-First of Array: 解説 T extends U ? X :

    Y : Conditional Types T extends U の関係を満たすときは X 、さもなくば Y を返す型版の条件演算子 型引数 <...> 以外で extends を使うときはほぼConditional Types 完成形 type First<T extends any[]> = T extends [] ? never : T[0] ` ` ` ` ` ` ` ` ` ` ` ` type R1 = 1 extends number ? "YES!" : "NO!" // "YES!" 型になる type R2 = "x" extends boolean ? true : false // false 型になる // T extends [] ? never : T[0] のT に[] 型を渡した時 type R3 = [] extends [] ? never : [][0] // never 型になる
  21. 初級5-Length of Tuple: 問題 例えば: タプル T を受け取り、そのタプルの長さを返す型 Length<T> を実装します。

    ` ` ` ` type tesla = ['tesla', 'model 3', 'model X', 'model Y'] type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT'] type teslaLength = Length<tesla> // expected 4 type spaceXLength = Length<spaceX> // expected 5
  22. 初級5-Length of Tuple: 問題 T["length"] : プロパティに対するIndexed Access Types JS

    において、配列オブジェクトの length プロパティは配列の要素数を返す。またプロパティに対するインデ ックス参照と . によるアクセスは同義。 そして、タプル型は長さが決まっているので、タプル型の length プロパティの型は数値リテラル型になる。 完成形 type Length<T extends any[]> = T["length"] ` ` ` ` ` ` const arr = [1, 2, 3] arr.length // 3 になる arr["length"] // インデックス参照と「. 」によるアクセスは同義 ` `
  23. 初級6-Exclude: 問題 例えば: 組み込みの型ユーティリティ Exclude <T, U> を使用せず、 U に割り当て可能な型を

    T から除外する型を 実装します。 ` ` ` ` ` ` type Result = MyExclude<1 | 2 | 3 | 4, 1 | 3> // 2 | 4 になる
  24. 初級6-Exclude: 解説 Distributive Conditional Types (分配的なConditional Types ) 例: MyExclude<T,

    U> の T にユニオン型 1 | 2 を渡した時、以下のように分配される これは数学における掛け算の分配法則と同じ より正確には T extends U ? X : Y の T にユニオン型が渡されたときに起こる。 完成形 type MyExclude<T, U> = T extends U ? never : T ` ` ` ` ` ` (1 | 2) extends U ? never : T => (1 extends U ? never : 1) | (2 extends U ? never : 2) (1 + 2) × u = (1 × u) + (2 × u) ` ` ` `
  25. 初級6-Exclude: 解説 また never 型とそれ以外の型とのユニオン型をとると never 型は消える性質がある 完成形 type MyExclude<T,

    U> = T extends U ? never : T ` ` ` ` MyExclude<1 | 2 | 3, 2> => (1 extends 2 ? never : 1) | (2 extends 2 ? never : 2) | (3 extends 2 ? never : 3) => 1 | never | 3 => 1 | 3
  26. 初級7-Awaited: 問題 例えば: Promise<ExampleType> という型がある場合、どのようにして ExampleType を取得すればよいでし ょうか。 Promise ライクな型が内包する型をどのように取得すればよいでしょうか。

    ` ` type R1 = MyAwaited<Promise<string>> // string type R2 = MyAwaited<Promise<{ field: number }>> // { field: number } type R3 = MyAwaited<Promise<Promise<string | number>>> // string | number type R4 = MyAwaited<Promise<Promise<Promise<string | boolean>>>> // string | boolean type Err = MyAwaited<string> // Promise ではないのでエラー
  27. 初級7-Awaited: 解説 infer U : 型推論 Conditional Types ( X

    extends Y ? L : R )の Y 以降に使えるキーワード X extends Y<infer Z> において X extends Y<any> を満たすとき、 any の部分にマッチする型を型 変数 Z に代入し、 L の部分で使えるようにする。 これだけだと以下のようにネストされたPromise に対応できない。 惜しい実装 type MyAwaited<T extends Promise<any>> = T extends Promise<infer U> ? U : never; ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` MyAwaited<Promise<string>> => Promise<string> extends Promise<infer U /* string に推論 */> ? U : never => string MyAwaited<Promise<Promise<string>>> => Promise<Promise<string>> extends Promise<infer U> ? U : never => Promise<string>
  28. 初級7-Awaited: 解説 例 推論された U が Promise<any> か検証し、そうであれば再帰呼び出しすることで解決 ` `

    ` ` type MyAwaited<T extends Promise<any>> = T extends Promise<infer U> ? U extends Promise<any> ? MyAwaited<U> : U : never; MyAwaited<Promise<Promise<string>>> => Promise<Promise<string>> extends Promise<infer U /* Promise<string> に推論 */ > ? Promise<string> extends Promise<any> ? MyAwaited<Promise<string>> : Promise<string> : never; => MyAwaited<Promise<string>> => Promise<string> extends Promise<infer U /* string に推論 */ > ? string extends Promise<any> ? MyAwaited<string> : string : never; => string
  29. 初級8-If: 問題 例えば: 条件値 C 、 C が truthy である場合の戻り値の型

    T 、 C が falsy である場合の戻り値の型 F を受け取る If を実装します。 条件値 C は true か false のどちらかであることが期待されますが、 T と F は任 意の型をとることができます。 ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` type A = If<true, 'a', 'b'>; // expected to be 'a' type B = If<false, 'a', 'b'>; // expected to be 'b'
  30. 初級8-If: 解説 急にすごい簡単になった… ちなみに boolean は true | false と等価

    完成形 type If<C extends boolean, T, F> = C extends true ? T : F ` ` ` `
  31. 初級9-Concat: 解説 完全知識問題 [...T] : Variadic Tuple Types (可変なタプル型) T

    がタプル型であるとき、 T を一部分に展開した新たなタプル型を返す 完成形 type Concat<T extends any[], U extends any[]> = [...T, ...U] ` ` ` ` ` ` type T1 = ["a", "b", "c"] type T2 = ["start", ...T1, "end"] // ["start", "a", "b", "c", "end"] になる
  32. 初級10-Includes: 問題 例えば: ただし、 Equal<T, U> は使ってもよいです。 JavaScript の Array.include

    関数を型システムに実装します。この型は、2 つの引数を受け取り、 true や false を出力しなければなりません。 ` ` ` ` ` ` type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false` ` ` Equal<1, 1> // true Equal<1, "a"> // false
  33. 初級10-Includes: 解説 理想的にはfor 文のように T から要素を1 つづつ取り出して U と比較し、一致していれば true

    を返したい。 だがfor 文は無いので再帰を考える。 1. T が [] なら false を返す 2. T の先頭の要素の型と U が等しいかを比較する 等しいなら true を返す 3. 等しくないなら T から先頭の要素を取り除いたタプル R を用意する 4. Includes<R, U> を返す 素朴にわかる範囲まで実装 type Includes<T extends any[], U> = /* 後で考える */ ? true : false ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` // 擬似コード includes([1, 2, 3], 2) => 1 != 2 なので return includes([2, 3], 2) => 2 == 2 なので return true
  34. 初級10-Includes: 解説 タプル T から先頭 H と残り R の型を取り出し、空なら false

    を返すところまで実装 T extends [infer H, ...infer R] ? X : Y Conditional Types とVariadic Tuple Types と型推論の組み合わせ(TS4.0 の新機能) T がタプル型のとき、 H には T の先頭要素の型に、 R には T から先頭を取り除いたタプル型が入る 例: [1, 2, 3] extends [infer H, ...infer R] → H は 1 、 R は [2, 3] 例: [1] extends [infer H, ...infer R] → H は 1 、 R は [] 例: [] extends [infer H, ...infer R] → そもそも成り立たず、 Y が呼ばれる 再帰ステップの1 と2 の途中まで実装 ` ` ` ` ` ` ` ` type Includes<T extends any[], U> = T extends [infer H, ...infer R] ? /* あとで考える */ : false; ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
  35. 初級10-Includes: 解説 完成形 あとは H と U を比較し、等しければ true を返し、さもなくば

    Includes<R, U> を返せばよい タプル T から先頭 H とそれ以外 R を取り出せた ` ` ` ` ` ` type Includes<T extends any[], U> = T extends [infer H, ...infer R] ? /* あとで考える */ : false; ` ` ` ` ` ` ` ` type Includes<T extends any[], U> = T extends [infer H, ...infer R] ? Equal<H, U> extends true ? true : Includes<R, U> : false;
  36. 初級10-Includes: 解説 例: T が [1, 2, 3] 、 U

    が2 のとき 完成形 type Includes<T extends any[], U> = T extends [infer H, ...infer R] ? Equal<H, U> extends true ? true : Includes<R, U> : false; ` ` ` ` ` ` Includes<[1, 2, 3], 2> => [1, 2, 3] extends [infer H /* 1 */, ...infer R /* [2, 3] */] ? Equal<1, 2> extends true ? true : Includes<[2, 3], 2> : false; => Includes<[2, 3], 2> => [2, 3] extends [infer H /* 2 */, ...infer R /* [3] */] ? Equal<2, 2> extends true ? true : Includes<[3], 2> : false; => true
  37. 初級13-Parameters: 問題 例えば: 組み込みの型ユーティリティ Parameters<T> を使用せず、 T からタプル型を構築する型を実装します。 ` `

    ` ` type F = (arg1: string, arg2: number) => void // 関数の型 // 関数の型の使用例 function f(arg1: string, arg2: number) { console.log("foo") } const f1: F = f // 関数の型のアロー関数での使用例 const f: F = (arg1: string, arg2: number): void => { console.log("foo") } type FunctionParamsType = MyParameters<F> // [string, number] になる
  38. 初級13-Parameters: 解説 infer は関数の型に対しても使える 完成形 ` ` type MyParameters<T extends

    (...args: any[]) => any> = T extends (...args: infer U) => any ? U : never;