Command Palette

Search for a command to run...

Hectal
PHASE 10Advanced ~23 min· topic 2 of 6

Topic 10.2

Union and Function Utility Types

In one line

Exclude, Extract and NonNullable filter a union, while ReturnType, Parameters, InstanceType, Awaited and the this helpers pull types out of functions and classes. All are small conditional types using infer.

Think of it like this

A fruit basket sorter. Tip the basket onto a belt: a gate lets only apples through (Extract), another gate throws apples away and lets the rest through (Exclude). A label reader at the end of a different belt reads what is written on a parcel's label without opening the box (ReturnType, Parameters). Each utility looks at one thing at a time and decides.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Conditional type
A choice between two types: T extends U ? X : Y.
Distribute
When a conditional type gets a union, it is applied to each member separately and the answers are joined.
`never`
The empty type: no value fits. In a union it disappears, so "a" | never is just "a".
`infer`
Inside a conditional type, gives a name to a piece the checker finds, like the return type.
Tuple
A fixed-length array type such as [name: string, age: number].
`typeof f`
In a type position, the type of the value f (a function, a class, a config object).
Thenable
An object with a then method, like a Promise. await unwraps thenables.
Intrinsic
A type implemented inside the compiler instead of in the library files, such as NoInfer and Uppercase.

Step by step

01Read the real definitions

These lines are copied from TypeScript 6.0's lib.es5.d.ts. Each fits on one line except Awaited, which is longer and comes last, with the library's own comments.

Notice NonNullable is not a conditional type any more, and NoInfer has no body at all: the compiler provides it.

lib.es5.d.tswhole filets
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type NonNullable<T> = T & {};
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
type ThisParameterType<T> = T extends (this: infer U, ...args: never) => any ? U : unknown;
type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;
type NoInfer<T> = intrinsic;

type Awaited<T> = T extends null | undefined ? T : // special case for `null | undefined` when not in `--strictNullChecks` mode
    T extends object & { then(onfulfilled: infer F, ...args: infer _): any; } ? // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
        F extends ((value: infer V, ...args: infer _) => any) ? // if the argument to `then` is callable, extracts the first argument
            Awaited<V> : // recursively unwrap the value
        never : // the argument to `then` was not callable
    T; // non-object or non-thenable

02Exclude and Extract walk a union

Given "click" | "scroll" | "keydown" | "keyup", Extract<..., key${string}> asks of each member: does it fit the pattern "key followed by anything"? Two do.

Exclude is the same walk with the answers swapped. Remember: the walk only happens because T is a bare type parameter on the left of extends.

Exclude and Extract walk a uniondiagram
Rendering diagram…

03Pull types out of functions

ReturnType<typeof makeUser> is the object type the function returns, with no copy-pasting. Change the function and the type follows.

Parameters<typeof makeUser> is the tuple [name: string, age: number]. Spread it with ...args to forward arguments from a wrapper function.

04Rebuild them with infer

Write MyReturn with infer R in the return position and MyParams with infer P in the rest-parameter position. The results match the built-ins.

The constraint T extends (...args: any) => any is why ReturnType<string> is an error: a string is not a function.

05Awaited, this and NoInfer

Awaited<ReturnType<typeof load>> peels the Promise off an async function's result. OmitThisParameter gives the type .bind returns.

NoInfer stops the default argument from joining the guessing. Without it, "green" would widen T to include "green"; with it, T comes from the options array only and "green" is an error.

Try it yourself

  1. 1

    Distribution at work

    In the first example, add type Q = MyExtract<"a" | 1 | true, string | number>; with a // ^?. Predict the answer before you look: the check runs once for each member.

  2. 2

    Turn distribution off

    Write type NoSplit<T> = [T] extends [string] ? "yes" : "no"; and ask NoSplit<"a" | 1>. Wrapping both sides in [ ] stops the walk over the union (Topic 9.2), so you get one answer for the whole union.

  3. 3

    Forward arguments

    In the second example write function logged(...args: Parameters<typeof makeUser>) { return makeUser(...args); } and hover logged. Then add a third parameter to makeUser and watch logged follow.

Code & diagrams

Exclude, Extract and NonNullable, then the same by hand

The home-made versions give the same answers. The error is expected: 'click' is not one of the key events.

Console

(nothing was printed)

Expected console output

keyup click
ReturnType, Parameters, ConstructorParameters, InstanceType

Parameters keeps the names as tuple labels. The last line is an error: string is not a function, so ReturnType refuses it.

Console

(nothing was printed)

Expected console output

{ name: 'Asha', age: 30, active: true }
Awaited, the this helpers and NoInfer TypeScript 5.4+

NoInfer (5.4) keeps the default value from widening T, so the second call is an error but still runs.

Console

(nothing was printed)

Expected console output

Hello, Asha
red red

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Ask ReturnType about a non-function

At the end of the second example, type Oops = ReturnType<string>;.

terminal
$ Types panel
── what you'll see ──
index.ts(26,24): error TS2344: Type 'string' does not satisfy the constraint '(...args: any) => any'.

Break #2

Assign a value the filter removed

In the first example, const bad: Keys = "click";

terminal
$ Types panel
── what you'll see ──
index.ts(20,7): error TS2322: Type '"click"' is not assignable to type 'Keys'.

Myth vs fact

Myth

ReturnType<typeof fn> and ReturnType<fn> are the same.

Fact

fn is a value, not a type, so the second is an error. typeof fn turns the value into its type.

Myth

Exclude removes properties from an object type.

Fact

It filters the members of a union. To drop properties use Omit (Topic 10.1).

Myth

NonNullable<T> is still a conditional type.

Fact

In current versions it is T & {}. The intersection with {} removes null and undefined, and the checker still knows that a NonNullable<T> can be used where a T or a {} is expected while T is an unknown type parameter.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    ReturnType and Parameters on an overloaded function see only the last overload signature. If you need another one, write an explicit conditional or pick it by hand. Generic functions also lose their type parameters: they collapse to their constraints, usually unknown.

  • ▸

    Awaited<T> exists because await is recursive and because a bare Promise<T> unwrap type was wrong for thenables and for T still unknown. The library signatures of Promise.all, Promise.race and Promise.resolve use it (ReturnType of an async function does not: it stays Promise<...>). Its special case for null | undefined is for code compiled without strictNullChecks.

  • ▸

    Distribution happens only for a naked type parameter. T extends U ? ... distributes; [T] extends [U] ? ... and T[] extends U[] ? ... do not. Debugging a wrong union result starts with asking whether the check was on a naked T.

Remember this

  1. 1

    The problem this solves: You often need to filter a union of types or read the types out of a function, such as its return value, without copying them by hand. These utilities are conditional types (Topic 9.1): T extends U ? X : Y asks "does T fit inside U?" and picks one of two answers. When T is a bare type parameter and you give it a union, the conditional distributes (Topic 9.2): it runs once per member and joins the answers. That is the whole trick behind Exclude and Extract.

  2. 2

    Exclude<T, U> is T extends U ? never : T: members that fit in U become never, and never vanishes from a union, so they disappear. Extract<T, U> is the mirror: T extends U ? T : never keeps only the members that fit. NonNullable<T> removes null and undefined; in current versions it is T & {}, an intersection with the empty object type, instead of a conditional.

  3. 3

    ReturnType<F> gives what a function returns, and Parameters<F> gives its parameters as a tuple (a fixed-length array type, Topic 5.2) with the parameter names kept as labels. Both are written with infer (Topic 9.3): "if F looks like a function, call whatever is in this position R". You almost always feed them typeof someFunction (Topic 8.2) because a function's type is not a name you can write.

  4. 4

    ConstructorParameters<C> and InstanceType<C> do the same for a class: the arguments new takes, and the object new makes. You pass typeof Point (the class value) and not Point (the instance type), because the class constructor is the thing being inspected.

  5. 5

    Awaited<T> is what await gives. For Promise<number> it is number, and it keeps unwrapping: Promise<Promise<number>> is also number. It matters with async functions, whose return type is already a Promise<...>, so Awaited<ReturnType<typeof load>> gets the final data type. It is recursive and only unwraps objects that have a callable then.

  6. 6

    ThisParameterType<F> extracts the type of a function's fake this parameter (Topic 2.6), and OmitThisParameter<F> removes it, which is exactly the type you get back from .bind(...). NoInfer<T> (TypeScript 5.4) tells the checker "don't use this position to work out T", so other arguments decide it (Topic 7.6). It is one of the few types built into the compiler itself (with Uppercase and friends) instead of being written in a library file.

Explain it without notes

01

How are Exclude and Extract built, and why do they work on unions?

02

How would you write ReturnType yourself?

03

What is the difference between ReturnType<typeof f> and Awaited<ReturnType<typeof f>> for an async function?

04

What does NoInfer do and when do you need it?

Practice

01

Given type Shape = { kind: "circle"; r: number } | { kind: "square"; side: number }, write Circle using only Extract and a kind pattern.

02

Write a function wrap(...args: Parameters<typeof makeUser>) that returns makeUser(...args), and a type Wrapped for its return type using ReturnType.

Trade-offs

  • ↔

    Deriving types from functions (ReturnType, Parameters) keeps one source of truth, but couples the types to an implementation. For a public API, write the type by hand so a refactor can't change it silently.

  • ↔

    Overloads, generics and this make these utilities lossy (last overload only, generics collapse). Know the limits before you build on them.

Done when you can

  • I can write Exclude, Extract and NonNullable and explain distribution.

  • I can use ReturnType, Parameters, ConstructorParameters and InstanceType with typeof.

  • I can write ReturnType and Parameters myself with infer.

  • I can get the resolved data type of an async function with Awaited.

  • I can explain OmitThisParameter and use NoInfer (5.4) to stop a position from widening a type parameter.