Why ts-toolbelt library use "O extends unknown" expression

Viewed 299

I am studying the source code of the ts-toolbelt library. And I often meet this expressionO extends unknown. In my opinion, it does not add any functionality.

And so I wondered, what is it for?

/**
 * @hidden
 */
export type _UnionOf<O extends object> =
    O[keyof O]

/**
 * Transform an [[Object]] into an [[Union]]
 * @param O to transform
 * @returns [[Any]]
 * @example
 * ```ts
 * ```
 */
export type UnionOf<O extends object> =
    O extends unknown
    ? _UnionOf<O>
    : never

For some reason, instead of exporting the _UnionOf type, it is preceded by the expression O extends unknown

2 Answers

Presumably the library authors decided that UnionOf<A | B | C> should produce the same result as UnionOf<A> | UnionOf<B> | UnionOf<C>, and the definition of _UnionOf did not do that. The O extends unknown ? ... : never check, which deceptively looks like it does nothing, causes that to happen.

Expressions that look like they do nothing but actually distribute across unions, when T is a generic type parameter:

  • T extends unknown ? ... : never
  • T extends any ? ... : never
  • T extends T ? ... : never

If T is a type parameter, as in the generic function function foo<T>(/*...*/): void or the generic interface interface Foo<T> {/*...*/}, then a type of the form T extends XXX ? YYY : ZZZ is a distributive conditional type.

It distributes the conditional check across unions in T. When T is specified with some specific type, the compiler splits that type up into its union members, evaluates the check for each such member, and then joins the results back into a new union. So if F<T> distributes across unions, then F<A | B | C> will be the same as F<A> | F<B> | F<C>.


Not all type functions are distributive across unions. For example, the keyof operator does not turn unions of inputs into unions of outputs:

type KeyofA = keyof {a: string}; // "a"
type KeyofB = keyof {b: number}; // "b"
type KeyofAorB = keyof ({ a: string } | { b: number }); // never

Similarly, _UnionOf is not distributive across unions (related to the fact that it uses keyof in its definition):

type Oops = _UnionOf<{ a: string } | { b: number }>
// never

If you have a type function which is not distributive in unions and you want it to be, you can wrap it in a distributive conditional type:

type DistribKeyof<T> = T extends unknown ? keyof T : never;

type UnionKeyofAorB = DistribKeyof<{ a: string } | { b: number }>; // "a" | "b"

And therefore UnionOf is distributive across unions:

type Correct = UnionOf<{ a: string } | { b: number }>
// string | number

Playground link to code

O extends unknown ? _UnionOf<O> : never is a conditional type; here it is used as a distributive conditional type. Without using a conditional type, TypeScript will narrow the type to one that applies to all possible union values (often never). With distributive conditional types, it will apply to each potential union value. Though the conditional aspect of distributive conditional types could be used to filter the potential union values, here that is unnecessary, so you'll frequently see values like T extends any (as in the handbook) or T extends unknown here.

As for the difference between those, dragomirtitian describes it in the Typescript gitter.im:

any sometimes deletes information about the type parameter. There was a bug around this I filed myself microsoft/TypeScript#30569. It should work ok now, but I would still recommend one of the other two ways [ed: T extends unknown or T extends T]. Any extends clause in a conditional type introduces information about T, extends any can confuse the compiler about T

See also:

Related