Item 53: Know How to Control the Distribution of Unions over Conditional Types
May 10, 2024 ยท View on GitHub
Things to Remember
- 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
booleanandnevertypes when they distribute over unions.
Code Samples
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
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(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'.
let dateOrStr = Math.random() < 0.5 ? new Date() : 'A';
// ^? let dateOrStr: Date | string
isLessThan(dateOrStr, 'B') // ok, but should be an error
type Comparable<T> =
[T] extends [Date] ? Date | number:
[T] extends [number] ? number :
[T] extends [string] ? string :
never;
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'.
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 PairOfStrings = NTuple<string, 2>;
// ^? type PairOfStrings = [string, string]
type TripleOfNumbers = NTuple<number, 3>;
// ^? type TripleOfNumbers = [number, number, number]
type PairOrTriple = NTuple<bigint, 2 | 3>;
// ^? type PairOrTriple = [bigint, bigint]
type NTuple<T, N extends number> =
N extends number
? NTupleHelp<T, N, []>
: never;
type PairOrTriple = NTuple<bigint, 2 | 3>;
// ^? type PairOrTriple = [bigint, bigint] | [bigint, bigint, bigint]
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!"
type CelebrateIfTrue<V> = [V] extends [true] ? 'Huzzah!' : never;
type SurpriseParty = CelebrateIfTrue<boolean>;
// ^? type SurpriseParty = never
type AllowIn<T> = T extends {password: "open-sesame"} ? "Yes" : "No";
type N = AllowIn<never>;
// ^? type N = never