TL;DR
- 条件型がユニオン型に分配されるのは、条件に型パラメータがそのまま使われている(
T extends ...)場合だけである。 - 分配を無効にするには、条件を 1 要素のタプルで包む(
[T] extends [...])。
内容
分配が起きる理由
(string | number) extends string は false である。それでも次の double に string | number を渡すと、string | number が返る。
declare function double<T extends string | number>(x: T): T extends string ? string : number
double("a") // string を返す
double(1) // number を返す
double("a" as string | number) // string | number を返す条件型がユニオン型の各要素に「分配」されるので、string | number が返る。
(string | number) extends string ? string : number
-> (string extends string ? string : number | number extends string ? string : number)
-> string | number訳者の解説によると、本書の中でも特に難しいトピックの 1 つである。この項目では、分配がどんな条件で起きるのか、それを利用して分配を有効・無効にする方法を示している。
分配を無効にする
条件型が分配されるのは、条件に型パラメータがそのまま使われている場合だけである。そのため、条件を 1 要素のタプルで包めば分配を無効にできる。
// 修正前
type Comparable<T> = T extends Date
? Date | number
: T extends number
? number
: T extends string
? string
: never
declare function isLessThan<T>(a: T, b: Comparable<T>): boolean
isLessThan(Math.random() < 0.5 ? new Date() : "A", "B") // OK// 修正後
type Comparable<T> = [T] extends [Date]
? Date | number
: [T] extends [number]
? number
: [T] extends [string]
? string
: never
isLessThan(Math.random() < 0.5 ? new Date() : "A", "B") // 型エラー分配させる
逆に分配させたいときは、条件が T extends ... の形になるようにする。
// 修正前
type NTuple<T, N extends number> = NTupleHelp<T, N, []>
type NTupleHelp<T, N extends number, Acc extends T[]> = Acc["length"] extends N
? Acc
: NTupleHelp<T, N, [T, ...Acc]>
type PairOrTriple = NTuple<bigint, 2 | 3> // [bigint, bigint]// 修正後
type NTuple<T, N extends number> = N extends number ? NTupleHelp<T, N, []> : never
type PairOrTriple = NTuple<bigint, 2 | 3> // [bigint, bigint] | [bigint, bigint, bigint]boolean と never に注意する
boolean は true と false のユニオン型なので、条件型に使うと分配が起きる。
type CelebrateIfTrue<V> = V extends true ? "Huzzah!" : never
type SurpriseParty = CelebrateIfTrue<boolean>
// = CelebrateIfTrue<true> | CelebrateIfTrue<false>
// = 'Huzzah!' | never
// = 'Huzzah!'never は空のユニオンとして扱われる。条件型に使うと分配する対象がないので、never が返る。
type AllowIn<T> = T extends { password: "open-sesame" } ? "Yes" : "No"
type N = AllowIn<never> // neverどちらも、条件を 1 要素のタプルで包めば避けられる。ただし never はすべての型のサブタイプなので、包むだけだと AllowIn<never> が 'Yes' になる。先に never かどうかを判定する一手間が必要である。
type CelebrateIfTrue<V> = [V] extends [true] ? "Huzzah!" : never
type AllowIn<T> = [T] extends [never]
? "No"
: [T] extends [{ password: "open-sesame" }]
? "Yes"
: "No"キーワード
自分の理解
条件型の分配は『プログラミング TypeScript』にも出てきたが、完全に忘れていた。