Slide 1

Slide 1 text

TypeScript Quiz @uhyo_ 2024-03-28 Encraft #12 Frontend Quiz Night

Slide 2

Slide 2 text

前提 今回の問題では、TypeScriptバージョンは5.4.3(最新版) コンパイラオプションは { “strict”: true } を前提としています。

Slide 3

Slide 3 text

出題者紹介 uhyo 株式会社カオナビ フロントエンドエンジニア 最近はTypeScriptリポジトリのissueを ウォッチするのが趣味。

Slide 4

Slide 4 text

第1問 この関数getHelloOrWorldの返り値の型は “hello” | “world” 型ですが…… function getHelloOrWorld() { if (Math.random() < 0.5) { return "hello"; } else { return "world"; } }

Slide 5

Slide 5 text

第1問 では、この関数getHelloの返り値の型はどれ? function getHello() { return "hello"; } 1: string 2: “hello” 3: “hello” & string 4: 型エラー

Slide 6

Slide 6 text

第1問 では、この関数getHelloの返り値の型はどれ? function getHello() { return "hello"; } 1: string 2: “hello” 3: “hello” & string 4: 型エラー 正解は……

Slide 7

Slide 7 text

第1問 では、この関数getHelloの返り値の型はどれ? function getHello() { return "hello"; } 1: string

Slide 8

Slide 8 text

第1問 解説 返り値に表れるリテラル型が1つか2つ以上かによって 挙動が異なる。 // string function getHello() { return "hello"; } // “hello” | “world” function getHelloOrWorld() { if (Math.random() < 0.5) { return "hello"; } else { return "world"; } }

Slide 9

Slide 9 text

第1問 解説 実装当初から一貫してこの挙動で、理由はPR内で 説明されている。 The issue is that you practically never want the literal type. After all, why write a function that promises to always return the same value? https://github.com/microsoft/TypeScript/pull/10676#issuecomment-244257359 (要約)常に特定の値を返す関数なんて書かんでしょ

Slide 10

Slide 10 text

第1問 解説 返り値が1つでもリテラル型のほうがいい場合は as constを使う。 // “hello” function getHello() { return "hello" as const; }

Slide 11

Slide 11 text

第1問 解説 ちなみに、ユニオン型の場合も let変数に入れるとstringに なってしまう。 (この辺りの細かい挙動はwidening で調べてみよう) function getHelloOrWorld() { if (Math.random() < 0.5) { return "hello"; } else { return "world"; } } let str = getHelloOrWorld(); // ^? let str: string

Slide 12

Slide 12 text

第2問 このコードで、KeysとValuesはど んな型? (選択肢は Keys / Values の形) const values = { foo: 123, bar: 456, } satisfies Record; type Keys = keyof typeof values; type Values = typeof values[Keys]; 1: string / number 2: “foo” | “bar” / number 3: string / 123 | 456 4: “foo” | “bar” / 123 | 456

Slide 13

Slide 13 text

第2問 このコードで、KeysとValuesはど んな型? (選択肢は Keys / Values の形) const values = { foo: 123, bar: 456, } satisfies Record; type Keys = keyof typeof values; type Values = typeof values[Keys]; 1: string / number 2: “foo” | “bar” / number 3: string / 123 | 456 4: “foo” | “bar” / 123 | 456 正解は……

Slide 14

Slide 14 text

第2問 このコードで、KeysとValuesはど んな型? (選択肢は Keys / Values の形) const values = { foo: 123, bar: 456, } satisfies Record; type Keys = keyof typeof values; type Values = typeof values[Keys]; 2: “foo” | “bar” / number

Slide 15

Slide 15 text

第2問 解説 satisfiesの特徴は、式が特定の型に当てはまるかどうかチェック しつつ、型推論の結果を尊重してくれること。 const values = { foo: 123, bar: 456, } satisfies Record; type Keys = keyof typeof values; type Values = typeof values[Keys];

Slide 16

Slide 16 text

第2問 解説 なので実はsatisfiesが 無くても結果が同じ。 const values = { foo: 123, bar: 456, }; type Keys = keyof typeof values; type Values = typeof values[Keys]; valuesの型は { “foo”: number; “bar”: number; } なのでKeysは “foo” | “bar”、 Valuesは number となる。

Slide 17

Slide 17 text

第2問 解説 このケースでsatisfiesを使う目的は、 オブジェクトのプロパティに数値以外が入るのを防ぐこと。 const values = { foo: 123, bar: 456, baz: “789”, // 型エラー } satisfies Record; type Keys = keyof typeof values; type Values = typeof values[Keys];

Slide 18

Slide 18 text

第2問 解説 このようにsatisfiesではなく型注釈を使うと、 Keysがstringになってしまう。 キーの情報を損ないたくない場合にsatisfiesが便利。 const values: Record = { foo: 123, bar: 456, }; type Keys = keyof typeof values; type Values = typeof values[Keys];

Slide 19

Slide 19 text

第2問 解説 const values = { foo: 123, bar: 456, } as const satisfies Record; type Keys = keyof typeof values; type Values = typeof values[Keys]; Valuesの型も具体的に欲しい場合は、 このようにas constと併用するとよい。 こうするとValuesは123 | 456 型になる。

Slide 20

Slide 20 text

第3問 今からお見せする4つのTypeScriptコードのうち、 1つだけ型エラーが発生するものがあります。 どれでしょう?

Slide 21

Slide 21 text

第3問 type Data1 = string | number; function useData1(data: Data1) { if (typeof data === "number") { 0 <= data && data <= 10; } } 1

Slide 22

Slide 22 text

第3問 type Data2 = { type: "success"; value: number; } | { type: "error"; error: unknown; } function useData2(data: Data2) { if (data.type === "success") { 0 <= data.value && data.value <= 10; } } 2

Slide 23

Slide 23 text

function useData3(data: object) { if ("foo" in data && typeof data.foo === "number") { 0 <= data.foo && data.foo <= 10; } } 3 第3問

Slide 24

Slide 24 text

第3問 function useData4( data: Record, key: string, ) { if (typeof data[key] === "number") { 0 <= data[key] && data[key] <= 10; } } 4

Slide 25

Slide 25 text

type Data1 = string | number; function useData1(data: Data1) { if (typeof data === "number") { 0 <= data && data <= 10; } } 1 type Data2 = { type: "success"; value: number; } | { type: "error"; error: unknown; } function useData2(data: Data2) { if (data.type === "success") { 0 <= data.value && data.value <= 10; } } 2 function useData3(data: object) { if ("foo" in data && typeof data.foo === "number") { 0 <= data.foo && data.foo <= 10; } } function useData4( data: Record, key: string, ) { if (typeof data[key] === "number") { 0 <= data[key] && data[key] <= 10; } } 1 3 4 第3問 型エラーが発生するのはどれ?

Slide 26

Slide 26 text

type Data1 = string | number; function useData1(data: Data1) { if (typeof data === "number") { 0 <= data && data <= 10; } } 1 type Data2 = { type: "success"; value: number; } | { type: "error"; error: unknown; } function useData2(data: Data2) { if (data.type === "success") { 0 <= data.value && data.value <= 10; } } 2 function useData3(data: object) { if ("foo" in data && typeof data.foo === "number") { 0 <= data.foo && data.foo <= 10; } } function useData4( data: Record, key: string, ) { if (typeof data[key] === "number") { 0 <= data[key] && data[key] <= 10; } } 1 3 4 第3問 型エラーが発生 するのはどれ? 正解は……

Slide 27

Slide 27 text

第3問 型エラーが発生するのは4。 1~3は型の絞り込みがうまくいくが、 4は絞り込みができない。

Slide 28

Slide 28 text

type Data1 = string | number; function useData1(data: Data1) { if (typeof data === "number") { 0 <= data && data <= 10; } } 1 type Data2 = { type: "success"; value: number; } | { type: "error"; error: unknown; } function useData2(data: Data2) { if (data.type === "success") { 0 <= data.value && data.value <= 10; } } 2 function useData3(data: object) { if ("foo" in data && typeof data.foo === "number") { 0 <= data.foo && data.foo <= 10; } } 1 3 第3問 解説 1~3は絞り込みが効くパターン。

Slide 29

Slide 29 text

第3問 解説 実は最近、4も絞り込みできるようにするPRが出た。

Slide 30

Slide 30 text

第3問 解説 function useData4( data: Record, key: string, ) { if (typeof data[key] === "number") { 0 <= data[key] && data[key] <= 10; } } 4 このように data[key] に対する絞り込みは、 keyがただのstring型の場合はできなかった。 次バージョンではCFAが拡張され、できるようになりそう。

Slide 31

Slide 31 text

第4問 type Not = B extends true ? false : true; type IsString = T extends string ? true : false; type IsNotString = Not>; type Output = IsNotString; このコードで、Output型は1~4のうちどれになる? 1: never 2: false 3: true 4: boolean

Slide 32

Slide 32 text

第4問 type Not = B extends true ? false : true; type IsString = T extends string ? true : false; type IsNotString = Not>; type Output = IsNotString; このコードで、Output型は1~4のうちどれになる? 1: never 2: false 3: true 4: boolean 正解は……

Slide 33

Slide 33 text

第4問 type Not = B extends true ? false : true; type IsString = T extends string ? true : false; type IsNotString = Not>; type Output = IsNotString; このコードで、Output型は1~4のうちどれになる? 4: boolean

Slide 34

Slide 34 text

第4問 解説 type Not = B extends true ? false : true; type IsString = T extends string ? true : false; type IsNotString = Not>; type Output = IsNotString; 条件型 (conditional types) の分配 (distribution) 知ってますか? という知識問題でした。 IsString = (string extends string ? true : false) | (number extends string ? true : false) = true | false = boolean 型の計算の流れ①

Slide 35

Slide 35 text

第4問 解説 type Not = B extends true ? false : true; type IsString = T extends string ? true : false; type IsNotString = Not>; type Output = IsNotString; 条件型 (conditional types) の分配 (distribution) 知ってますか? という知識問題でした。 IsNotString = Not> = Not = (true extends true ? false : true) | (false extends true ? false : true) = false | true = boolean 型の計算の流れ②

Slide 36

Slide 36 text

予備

Slide 37

Slide 37 text

第5問 type Not = B extends true ? false : true; type IsString = T extends string ? true : false; type IsNotString = Not>; type Output = IsNotString; // ^? Output = boolean 第4問のこのコードを、Output が true になるように修正します。 分配が起こらないように直せば修正できます。

Slide 38

Slide 38 text

第5問 type Not = B[] extends true[] ? false : true; type IsString = T[] extends string[] ? true : false; type IsNotString = Not>; type Output = IsNotString; // ^? Output = true 実は、このように直せば分配が起こらなくなり、修正できます。

Slide 39

Slide 39 text

第5問 type Not = B[] extends true[] ? false : true; type IsString = T[] extends string[] ? true : false; type IsNotString = Not>>>; type Output = IsNotString; // ^? Output = false しかし、調子に乗ってNotを3重にしたら壊れました。 Notの定義を変えることで、Outputがtrueとなる 正しい挙動に修正してください。

Slide 40

Slide 40 text

第5問 type Not = B[] extends true[] ? false : true; 修正前 type Not = readonly B[] extends readonly true[] ? false : true; 1 type Not = [B] extends [true] ? false : true; 2 type Not = {v:B} extends {v:true} ? false : true; 3 type Not = (()=>B) extends (()=>true) ? false : true; 4

Slide 41

Slide 41 text

第5問 type Not = B[] extends true[] ? false : true; 修正前 type Not = readonly B[] extends readonly true[] ? false : true; 1 type Not = [B] extends [true] ? false : true; 2 type Not = {v:B} extends {v:true} ? false : true; 3 type Not = (()=>B) extends (()=>true) ? false : true; 4 正解は……

Slide 42

Slide 42 text

第5問 type Not = B[] extends true[] ? false : true; 修正前 type Not = [B] extends [true] ? false : true; 2

Slide 43

Slide 43 text

第5問 解説 type Not = [B] extends [true] ? false : true; 2 修正前と1~4はどれも分配を避けられる書き方。 しかし、2のように[ ] で囲む(タプル型を使う)のが公式に推奨されるやり方で あり、ドキュメントにも書いてある。 https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types この問題の例のように、このやり方は型推論の面で少し優遇される。 参考: https://github.com/microsoft/TypeScript/issues/52068