技術とかの雑なToday I Learnedメモ

Conditional Typesとinferキーワード

Conditional Types と infer

TypeScript の条件型(Conditional Type)と infer キーワード - 30 歳からのプログラミング

公式ドキュメントを読んでも infer が理解できない人のための infer の説明 - Qiita

infer に入門するぞ!infer って何?というところから始めた。

infer は Conditional Types において型をキャプチャできる、と説明されてもなんにも分からなかったので、詳しく説明されている記事を読んで少しずつ理解した。

infer を知るには Generics と Conditional Types の二つを知る必要があり、Generics については全く知らんこともないという感じだったのでいったん置いといて Conditional Types を学んだ。

結局 Generics についても記事を読んだのでそれも貼っとく。

TypeScript のジェネリック型の初歩 - 30 歳からのプログラミング

Conditional Types

Conditional Types は条件型という名前であり、T extends U ? A : Bで表される。

T が U もしくは U のサブタイプだった場合(つまり T が U と互換性がある状態の場合)は A、そうでない場合は B になる。

読んだ記事に書いてあったけど、まんま三項演算子っぽい。

U が any だったら確定で A になる。

type Test<T> = T extends string | number ? T : never

type A = Test<'hoge'> // => type A は 'hoge'型
type B = Test<100> // => type B は 100型
type C = Test<{ a: 'aaa' }> // => type C は never型

extends は Generics で使われるもので、T に対して制約を与えるためのキーワード。Generics を普通に使うと、制約に違反した型は型エラーとなるが、Conditional Types ではエラーになるのではなく条件分岐のための判定として利用されている。

関係ないけど、100型というのは数字の 100 以外受け付けない型なので 100 しか入れられないね。

分配法則

これは知ったとき「これは Conditional Types 使うときに知ってないとハマりそうだな……」と思った。

T1 | T2 extends U ? A : Bは、僕が最初見たときは直感的に(T1 | T2) extends U ? A : Bのように思った。

つまり T1 と T2 のどちらかが U と互換性があれば A、そうでなければ B になる、という認識をしたが、実際はこうではない。

正しくは(T1 extends U ? A : B) | (T2 extends U ? A : B)になる。

type Test<T> = T extends string | number ? T : never

// Test<'hoge' | 100 | { a: 'aaa' }> は
// ('hoge' extends string | number ? T : never)
// と
// (100 extends string | number ? T : never)
// と
// ({ a: 'aaa' } extends string | number ? T : never)
// のUnion Typeとなる
// つまり
// ('hoge' extends string | number ? T : never) | (100 extends string | number ? T : never) | ({ a: 'aaa' } extends string | number ? T : never)
// になる
type A = Test<'hoge' | 100 | { a: 'aaa' }> // type A は 'hoge' | 100

となる。

ちなみにT | neverは T 。

infer

infer は、Conditional Types で一部の型をキャプチャできるキーワードで、T extends U ? A : Bの U の部分で使う。

T がT extends Uにマッチした際に、A で U の一部を使うことができる。

type Test<T> = T extends { a: infer U } ? U : never

type A = Test<{ a: 'aaa' }> // type A は 'aaa'型
type B = Test<{ b: 'bbb' }> // type B は never型
type C = Test<{ a: 'aaa'; b: 'bbb' }> // type B は 'aaa'型

ちなみにオブジェクトのキーが複数マッチする場合は、そのキーが持つ値の Union Type になった。

type Test<T> = T extends { [key: string]: infer U } ? U : never

type A = Test<{ a: 'aaa'; b: 'bbb' }> // type A は 'aaa' | 'bbb'型

記事にもあるが、infer は Conditional Types における Generics のようなものと考えることができるとあり、なるほどな〜と思った。

呼び出されるまで分からなくて、呼び出されて、かつ Conditional Types にマッチする場合に初めて型が決定する。これなら理解しやすい。素晴らしい記事に出会えて本当に感謝。