【TypeScript】Union TypeとConditional Typeの組み合わせに注意!Distributive Conditional Typeについて
Distributive Conditional Typeについて、これだけを抜き出した記事が調べてみてもあまり出てこないので、まとめてみようと思います。
結論
ConditionalTypeを使うときは、大体Generic Typeとセットなので、以下の挙動を理解して使いましょう。
(理解していれば、この記事を読む必要はないです。)
type Test1 = boolean extends true ? string : number; // Test1 => number型。
type Hoge2<T extends boolean> = T extends true ? string : number;
type Test2 = Hoge2<boolean>; // Test2 => string | number型。
// それが嫌な時はどうするんだろ。自分はこうしてる。
type Hoge3<T extends boolean> = [T] extends [true] ? string : number;
type Test3 = Hoge3<boolean>; // Test3 => number型。
根拠 => TypeScript: Documentation - Conditional Types
解説
boolean
はtrue
のサブタイプではないので、以下のConditional TypeはFalse Branchのnumber
型になります。
type Test1 = boolean extends true ? string : number; // Test1 => number型。
ところが、このboolean
の部分をGeneric Typeとしてくくりだすと、挙動が変わります。boolean
はtrue
とfalse
のUnion Typeなので、以下のようにstring | number型になります。
type Hoge2<T extends boolean> = T extends true ? string : number;
type Test2 = Hoge2<boolean>;
// => Hoge2<true | false>
// => (true extends true ? string : number) | (false extends true ? string : number) [★ここがdistributive(分配的)なふるまい]
// => string | number
一見奇妙な挙動ですが、ドキュメント「TypeScript: Documentation - Conditional Types」にもちゃんと書かれている挙動です。
例えば、Exclude
型を自作するときは便利です。
// see https://github.com/type-challenges/type-challenges/blob/main/questions/00043-easy-exclude/README.md
type MyExclude<T, U extends T> = T extends U ? never : T;
type Test = MyExclude<'a' | 'b' | 'c', 'a' | 'b'>
// => ('a' extends ('a' | 'b') ? never : 'a') | ('b' extends ('a' | 'b') ? never : 'b') | ('c' extends ('a' | 'b') ? never : 'c') [★ここがdistributive(分配的)なふるまい]
// => never | never | 'c'
// => 'c'
Union型に対して反復的な操作をする方法が、私の知る限りではこれしかないので、型パズルを解くときには必須のテクニックです。
もし、extends any
のConditional Typeがあったら、十中八九このDistributiveな振る舞いを狙っています。
type UnionArray<T> = T extends any ? T[] : never;
type Test = UnionArray<string | number> // Test => string[] | number[]
回避策
では、これが原因で問題が起こったら?つまり、Union型をそのままextends判定したかったら、どうすればよいのでしょうか?
自分は、タプル化すると大体うまくいくので、これでしのいでいます。
他にも良い案があったら教えてください。
// Distributiveにしてほしくない時
type Hoge<T extends boolean> = [T] extends [true] ? string : number;
type Test = Hoge<boolean>; // Test => number型。
// Distributiveにしてほしくない時 その2
type ToArray<T> = [T] extends [any] ? T[] : never;
type Test2 = ToArray<string | number> // Test2 => (string | number)[]
ただし、もし上記のようにする場合、ぱっと見て意味が分かりづらいコードなので、その旨をコメントで書いた方が良いですね。
(この記事へのリンクを書いてくれても良いんですよ!!!)
Never型に対するDistributive Conditional Type
Distributive Conditional Typeが原因で、never型は0個の型のUnion型と取られてちょっと不思議な挙動になります。
type Hoge<T extends boolean> = T extends true ? string : number;
type Test = Hoge<never>; // Test => never型
上記のHoge型は、string型かnumber型のどちらかになりそうですが、結果はnever型になります。多分ですが、never型を0個の型のUnion型ととらえているんだと思います。
(記事を書き始めてから調べたら、Typescript never type condition - Stack Overflowとかに書いてました。私もこのanswerと同意見です。)
先ほどと同様に、以下のようにすれば回避できます。
type Hoge<T extends boolean> = [T] extends [true] ? string : number;
type Test = Hoge<never>; // Test => string型
ちなみに、never型は全ての型のサブタイプなので、Conditional TypeでTrue Branchになります。
決してnumber型って書いてから動作確認して気付いたわけではないです。
記事紹介
ここまで読んでいただき、ありがとうございます。
最近、毎週投稿を初めて何とか続いているので、もしよければ他の記事も見て頂けると嬉しいです。
TypeScriptが好きなので、それ関連の投稿をしてます。