北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

TypeScriptの分配条件型

最近オライリーのTypeScript本を読んでいるが、型システムが頭がおかしくて(褒めてる)とても面白い。

www.oreilly.co.jp

TypeScriptでは、こんな感じの型レベル関数が定義できる。以下の Extract2T のうち、 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"

Extract2extends を使って定義されており、定義を読む限りは、 1 つ目の型が 2 つ目の型のサブタイプになっていれば 1 つ目の型を返すし、そうでなければ never を返すということになっている。以下の結果はこの考察と一致する。

type A2 = Extract2<string, "a">;
// type A2 = never
type A3 = Extract2<"a", string>;
// type A3 = "a"

しかし、 boolean で同じような操作をすると、 string の場合とは違い直感に反する結果になる。 booleantrue のサブタイプではないので、単純にサブタイプであることだけ確認しているのであれば、 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 型はリテラル型の有限個の合併では表現できないので、まあ妥当だろう)。実際、 A2string の代わりに合併型を使って試してみると、 never ではなくリテラル"a" が得られる。

type A6 = Extract2<"a" | "b" | "c" | "d", "a">;
// type A6 = "a"