最近オライリーのTypeScript本を読んでいるが、型システムが頭がおかしくて(褒めてる)とても面白い。
TypeScriptでは、こんな感じの型レベル関数が定義できる。以下の Extract2
は T
のうち、 U
であるものを展開するというものである。同様の働きをする Extract
が組み込みで定義されているので、ここでは 2
と名前を付けた。
type Extract2<T, U> = T extends U ? T : never;
Extract2
を使うと、例えばオブジェクト型のキーの型で、文字列であるものだけを型として取り出せる。
type O = { "name": string, "age": number, 0: number }; type A1 = Extract2<keyof O, string >; // type A1 = "name" | "age"
Extract2
は extends
を使って定義されており、定義を読む限りは、 1 つ目の型が 2 つ目の型のサブタイプになっていれば 1 つ目の型を返すし、そうでなければ never
を返すということになっている。以下の結果はこの考察と一致する。
type A2 = Extract2<string, "a">; // type A2 = never type A3 = Extract2<"a", string>; // type A3 = "a"
しかし、 boolean
で同じような操作をすると、 string
の場合とは違い直感に反する結果になる。 boolean
は true
のサブタイプではないので、単純にサブタイプであることだけ確認しているのであれば、 string
の場合と同様に A4
の結果は never
となるべきである。
type A4 = Extract2<boolean, true>; // type A4 = true type A5 = Extract2<true, boolean>; // type A5 = true
これはタイトルにもある通り、分配条件型という仕様によって実現されている。 Extract2<boolean, true>
は boolean
の定義に立ち返ると Extract2<true | false, true>
であり、 Extract2
の定義を展開すると (true | false) extends true ? (true | false) : never
となる。しかし、 TypeScript では |
を分配規則により展開し、実際は (true extends true ? true : never) | (false extends true ? false : never)
として扱う。よって、 true | never
と解釈され、それはつまり true
である。
冒頭のオブジェクトのキーの型を extract する例が期待通りに動作するのも、この仕様による。仮に string
が "a" | "b" | ... | "aa" | "ab" | ...
のような型であると解釈されて実装されていると、先ほどの A2
の型も "a"
となるのだろうが、 TypeScript ではそうなっていない( string
型はリテラル型の有限個の合併では表現できないので、まあ妥当だろう)。実際、 A2
を string
の代わりに合併型を使って試してみると、 never
ではなくリテラル型 "a"
が得られる。
type A6 = Extract2<"a" | "b" | "c" | "d", "a">; // type A6 = "a"