docs: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types ## 概要 [[TypeScript 条件型|条件型]] が [[TypeScript ジェネリクス|ジェネリクス型]] として振る舞うときに、==ユニオン型に対しては==分配法則を適用する。このような型は **Distributive conditonal type** と呼ばれる。 > When conditional types act on a generic type, they become distributive when given a union type 例えば、以下のようにジェネリクス型として条件型を記載した場合には直接的に条件型を記載する場合と結果が異なることがある。 ```ts // ジェネリクス型で記述した場合(悪い例) type IsSubtypeOf<Sub, Super> = Sub extends Super ? true : false; // 良い例(イテレーションを回避する) type IsSubtypeOf<Sub, Super> = [Sub] extends [Super] ? true : false; ``` > [!caution] > したがって、[[Subtyping MOC|部分型関係]] や [[TypeScriptのSubtypeとAssignment|割当互換性]] のチェックに上記のような書き方をおこなってしまうと予期しない結果となることがある。 もちろん、ジェネリクスにしなければ分配が発生しない。 ```ts type G<U, T> = U extends T ? 1 : 2; type A1 = number | string extends string ? 1 : 2; // => 2 type G1 = G<number | string, string>; //=> 1 | 2 type A2 = never extends never ? 1 : 2; // => 1 type G2 = G<never, never>; // => never ``` 条件部としてはほとんど機能していないが、イテレーションができるということで次のように記述する必要がある。次のコードでは、`Type extends any` の条件部が型変数がユニオン型の時に各要素に対してイテレーションされて、最終的にはイテレーション結果を合成したユニオン型が返される。 ```ts // 型変数には unknown も any も ok type ToArray<Type> = Type extends any ? Type[] : never; type StrOrNumArr = ToArray<string | number>; // string[] | number[] // boolean は true | false に分解されてしまう type BoolOrObj = ToArray<boolean | { a: 42 }>; /* type BoolOrObj = false[] | true[] | { a: 42; }[] */ ``` 具体的に何が起きているかというと、`StrOrNumArr` は次のように `string | number` に分配されて、ユニオン型の各要素の型にイテレーションしてマッピングされる。 ```ts ToArray<string> | ToArray<numebr>; // (string extends any ? string[] : never) | (number extends any ? number[]: never) ``` これによって最終結果は次のようになる。 ```ts string[] | numeber[]; ``` > Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of `T extends U ? X : Y` with the type argument `A | B | C` for `T` is resolved as `(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)`. > ([PRより](https://github.com/microsoft/TypeScript/pull/21316)) ## 分配法則の回避 分配法則は基本的に望まれる挙動だが、これを避けたい場合には [[TypeScript extendsでの型変数の制約|extends]] キーワード内部でブラケットで要素を囲むようにする。 > Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the `extends` keyword with square brackets. ```ts type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never; type StrArrOrNumArr = ToArrayNonDist<string | number>; // type StrArrOrNumArr = (string|number)[]; ``` また、基本的に分配では `any` よりも `unkown` を使ったほうがいいらしい。開発者より。 > `extends unknown` is generally preferred for creating distributivity > https://github.com/microsoft/TypeScript/issues/41349#issuecomment-724951863 ## never 型の発生 分配的になっている場合には [[TypeScript never型|never型]] が予期せず発生することがあるが、これは意図的なものであり、バグではない。[この issue](https://github.com/microsoft/TypeScript/issues/31751) で説明されている。 ```ts type IsSubtypeOf<Sub, Super> = Sub extends Super ? true : false; type MakeSense = IsSubtypeOf<{}, never>; // => false type Huh = IsSubtypeOf<never, never>; // => never (←ここ) ``` 公式 Wiki の Common "Bugs" That Aren't Bugs の箇所にリストされていた。 https://github.com/Microsoft/TypeScript/wiki/FAQ#common-bugs-that-arent-bugs > - This conditional type returns `never` when it should return the true branch. > - See this [issue](https://github.com/microsoft/TypeScript/issues/31751) for discussion about _distributive conditional types_. 条件型の分配は [[TypeScript ユニオン型|ユニオン型]] に対して行われ、`never` 型は要素 0 の [[Empty Typeとは|空型]] であるため、これに対して分配 (イテレーション) は決してできない。したがって、結果は `never` となる。 ここでは `never` 型が要素 0 のユニオン型としてみなされていることに注意。 逆に、どのような型も `never` (空集合) との合併と考えられるが、それなら分配的な条件型にどのような型を渡したとしてもこのようなことが発生しているともみなせる。代数的には `never` は和 (結び: join) 演算において [[加法単位元とは|加法単位元]] $0$ であるため、`A | never ≡ A ≡ never | A` となることから、分配条件型でイテレーションが仮に発生してそのユニオン型生成されたとしても元の型 `A` に吸収される。 ## unknown 側はユニオン型としてみなされない 一方 [[TypeScript unknown型|unknown型]] は全要素を持つユニオン型としてはみなされていない。`unknown` 型は `{} | null | undefined` として TypeScript は認識しているが、よくよく考えれば unknown 型にはオブジェクト型が入っているので何を持って一つの要素としてみなすかも明確ではない。 ```ts type C1 = IsAssignable<unknown, never>; // ^^: false type C2 = IsAssignable<unknown, unknown>; // ^^ true ``` `unknown` 型が条件型の分配でユニオン型としてみなされない挙動については以下の issue を参照。 https://github.com/microsoft/TypeScript/issues/27418 ```ts type F<T> = T extends number ? true : false; type A = F<never>; // never type B = F<unknown>; // false ``` ## any 型の特殊な挙動 [[TypeScript any型|any型]] についても挙動に注意する必要がある。→ [[TypeScript any型の分配法則]] ```ts type IsAssignable<Sub, Super> = Sub extends Super ? true : false; // イテレーションが発生しているような感じ type A1 = IsAssignable<any, string>; // ^^: boolean ``` ジェネリクス型にせずに直接書いた場合でもイテレーションが起きているような感じでユニオン型が発生する。 ```ts // ジェネリクス型ではないのにイテレーションが発生している? type A3 = any extends string ? true : false; // ^^: boolean type A4 = any extends string ? symbol : number; // ^^: number | symbol ``` これは代数的なものというよりも [[TypeScriptのSubtypeとAssignment|割当可能性]] は [[Subtyping MOC|部分型関係]] に `any` 型について双方向 (from/to) の割当を追加しているので、`any` 型を対象とした条件型の判定では、**真・偽の両方が当てはまってしまう**ということだと考えられる。つまり [[TypeScript extendsでの型変数の制約|extends]] での判定による両方の型のユニオン型を生成、つまり `? : true : false` の判定は `boolean` を生成してしまっていると考えられる。 実はこの特殊な挙動を利用して、[[TypeScript any型の検証|any型の検証]] ができる。