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

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

Distributive Conditional Typeについて、これだけを抜き出した記事が調べてみてもあまり出てこないので、まとめてみようと思います。

## 結論

ConditionalTypeを使うときは、大体Generic Typeとセットなので、以下の挙動を理解して使いましょう。
（理解していれば、この記事を読む必要はないです。）

```ts
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](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types)

## 解説

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

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

ところが、この`boolean`の部分をGeneric Typeとしてくくりだすと、挙動が変わります。`boolean`は`true`と`false`のUnion Typeなので、以下のようにstring | number型になります。

```ts
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](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types)」にもちゃんと書かれている挙動です。
例えば、`Exclude`型を自作するときは便利です。

```ts
// 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な振る舞いを狙っています。

```ts
type UnionArray<T> = T extends any ? T[] : never;
type Test = UnionArray<string | number> // Test => string[] | number[]
```

## 回避策

では、これが原因で問題が起こったら？つまり、Union型をそのままextends判定したかったら、どうすればよいのでしょうか？
自分は、タプル化すると大体うまくいくので、これでしのいでいます。
他にも良い案があったら教えてください。

```ts
// 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型と取られてちょっと不思議な挙動になります。

```ts
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](https://stackoverflow.com/questions/65492464/typescript-never-type-condition)とかに書いてました。私もこのanswerと同意見です。）
先ほどと同様に、以下のようにすれば回避できます。

```ts
type Hoge<T extends boolean> = [T] extends [true] ? string : number;
type Test = Hoge<never>; // Test => string型
```

ちなみに、never型は全ての型のサブタイプなので、Conditional TypeでTrue Branchになります。
~~決してnumber型って書いてから動作確認して気付いたわけではないです。~~

## 記事紹介

ここまで読んでいただき、ありがとうございます。
最近、毎週投稿を初めて何とか続いているので、もしよければ他の記事も見て頂けると嬉しいです。
TypeScriptが好きなので、それ関連の投稿をしてます。

- 10/01週_[「Announcing TypeScript 4.9 Beta」を読む](https://qiita.com/charon1212/items/1af23a4701ef9db0c8e0)
- 09/24週_[【TypeORM】ちゃんとinitializeしないと、「EntityMetadataNotFoundError」が出るよ](https://qiita.com/charon1212/items/aac45421798a70193d73)
- 09/17週_[[React]useContextとuseStateを使ったグローバル状態管理](https://qiita.com/charon1212/items/b8aabb09196fb8566b1c)
- 09/10週_[TypeScriptで配列の直積を計算する関数を、型込みで作成する（ついでに内積も）](https://qiita.com/charon1212/items/18e3e5d307b8f5eddd69)