5
0

More than 1 year has passed since last update.

【TypeScript】Union TypeとConditional Typeの組み合わせに注意!Distributive Conditional Typeについて

Posted at

【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

解説

booleantrueのサブタイプではないので、以下のConditional TypeはFalse Branchのnumber型になります。

type Test1 = boolean extends true ? string : number; // Test1 => number型。

ところが、このbooleanの部分をGeneric Typeとしてくくりだすと、挙動が変わります。booleantruefalseの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が好きなので、それ関連の投稿をしてます。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0