Skip to content

Item 53: Know How to Control the Distribution of Unions over Conditional Types

要点

  • Think about whether you want unions to distribute over your conditional types.
  • Know how to enable or disable distribution by adding conditions or by wrapping conditions in one-tuples.
  • Be aware of the surprising behavior of boolean and never types when they distribute over unions.
  • 考虑是否希望联合类型在你的条件类型上分发(distribute)。
  • 了解如何通过添加条件或将条件包装在单元素元组(one-tuples)中来启用或禁用分发。
  • 注意 booleannever 类型在分发到联合类型时的意外行为。

正文

ts
declare function double<T extends number | string>(
  x: T
): T extends string ? string : number

const num = double(12)
//    ^? const num: number
const str = double('x')
//    ^? const str: string

declare let numOrStr: number | string
const either = double(numOrStr)
//    ^? const either: number | string

💻 playground


ts
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

💻 playground


ts
isLessThan(new Date(), new Date()) // ok
isLessThan(new Date(), Date.now()) // ok, Date/number comparison allowed
isLessThan(12, 23) // ok
isLessThan('A', 'B') // ok
isLessThan(12, 'B')
//             ~~~ Argument of type 'string' is not assignable to parameter
//                 of type 'number'.

💻 playground


ts
let dateOrStr = Math.random() < 0.5 ? new Date() : 'A'
//  ^? let dateOrStr: Date | string
isLessThan(dateOrStr, 'B') // ok, but should be an error

💻 playground


ts
type Comparable<T> = [T] extends [Date]
  ? Date | number
  : [T] extends [number]
  ? number
  : [T] extends [string]
  ? string
  : never

💻 playground


ts
isLessThan(new Date(), new Date()) // ok
isLessThan(new Date(), Date.now()) // ok, Date/number comparison allowed
isLessThan(12, 23) // ok
isLessThan('A', 'B') // ok
isLessThan(12, 'B')
//             ~~~ Argument of type 'string' is not assignable to parameter
//                 of type 'number'.
isLessThan(dateOrStr, 'B')
//                    ~~~ Argument of type 'string' is not assignable to
//                        parameter of type 'never'.

💻 playground


ts
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]>

💻 playground


ts
type PairOfStrings = NTuple<string, 2>
//   ^? type PairOfStrings = [string, string]
type TripleOfNumbers = NTuple<number, 3>
//   ^? type TripleOfNumbers = [number, number, number]

💻 playground


ts
type PairOrTriple = NTuple<bigint, 2 | 3>
//   ^? type PairOrTriple = [bigint, bigint]

💻 playground


ts
type NTuple<T, N extends number> = N extends number
  ? NTupleHelp<T, N, []>
  : never

💻 playground


ts
type PairOrTriple = NTuple<bigint, 2 | 3>
//   ^? type PairOrTriple = [bigint, bigint] | [bigint, bigint, bigint]

💻 playground


ts
type CelebrateIfTrue<V> = V extends true ? 'Huzzah!' : never

type Party = CelebrateIfTrue<true>
//   ^? type Party = "Huzzah!"
type NoParty = CelebrateIfTrue<false>
//   ^? type NoParty = never
type SurpriseParty = CelebrateIfTrue<boolean>
//   ^? type SurpriseParty = "Huzzah!"

💻 playground


ts
type CelebrateIfTrue<V> = [V] extends [true] ? 'Huzzah!' : never

type SurpriseParty = CelebrateIfTrue<boolean>
//   ^? type SurpriseParty = never

💻 playground


ts
type AllowIn<T> = T extends { password: 'open-sesame' } ? 'Yes' : 'No'

💻 playground


ts
type N = AllowIn<never>
//   ^? type N = never

💻 playground

Released under the MIT License.