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" | neveris 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
thenmethod, like a Promise.awaitunwraps thenables. - Intrinsic
- A type implemented inside the compiler instead of in the library files, such as
NoInferandUppercase.
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.
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-thenable02Exclude 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.
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
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
Turn distribution off
Write
type NoSplit<T> = [T] extends [string] ? "yes" : "no";and askNoSplit<"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
Forward arguments
In the second example write
function logged(...args: Parameters<typeof makeUser>) { return makeUser(...args); }and hoverlogged. Then add a third parameter tomakeUserand watchloggedfollow.
Code & diagrams
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 clickParameters 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 }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 redBreak 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>;.
Break #2
Assign a value the filter removed
In the first example, const bad: Keys = "click";
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.
- ▸
ReturnTypeandParameterson 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, usuallyunknown. - ▸
Awaited<T>exists becauseawaitis recursive and because a barePromise<T>unwrap type was wrong for thenables and forTstill unknown. The library signatures ofPromise.all,Promise.raceandPromise.resolveuse it (ReturnTypeof an async function does not: it staysPromise<...>). Its special case fornull | undefinedis for code compiled withoutstrictNullChecks. - ▸
Distribution happens only for a naked type parameter.
T extends U ? ...distributes;[T] extends [U] ? ...andT[] extends U[] ? ...do not. Debugging a wrong union result starts with asking whether the check was on a nakedT.
Remember this
- 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 : Yasks "does T fit inside U?" and picks one of two answers. WhenTis 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 behindExcludeandExtract. - 2
Exclude<T, U>isT extends U ? never : T: members that fit inUbecomenever, andnevervanishes from a union, so they disappear.Extract<T, U>is the mirror:T extends U ? T : neverkeeps only the members that fit.NonNullable<T>removesnullandundefined; in current versions it isT & {}, an intersection with the empty object type, instead of a conditional. - 3
ReturnType<F>gives what a function returns, andParameters<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 withinfer(Topic 9.3): "if F looks like a function, call whatever is in this position R". You almost always feed themtypeof someFunction(Topic 8.2) because a function's type is not a name you can write. - 4
ConstructorParameters<C>andInstanceType<C>do the same for a class: the argumentsnewtakes, and the objectnewmakes. You passtypeof Point(the class value) and notPoint(the instance type), because the class constructor is the thing being inspected. - 5
Awaited<T>is whatawaitgives. ForPromise<number>it isnumber, and it keeps unwrapping:Promise<Promise<number>>is alsonumber. It matters with async functions, whose return type is already aPromise<...>, soAwaited<ReturnType<typeof load>>gets the final data type. It is recursive and only unwraps objects that have a callablethen. - 6
ThisParameterType<F>extracts the type of a function's fakethisparameter (Topic 2.6), andOmitThisParameter<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 (withUppercaseand friends) instead of being written in a library file.
Explain it without notes
How are Exclude and Extract built, and why do they work on unions?
How would you write ReturnType yourself?
What is the difference between ReturnType<typeof f> and Awaited<ReturnType<typeof f>> for an async function?
What does NoInfer do and when do you need it?
Practice
Given type Shape = { kind: "circle"; r: number } | { kind: "square"; side: number }, write Circle using only Extract and a kind pattern.
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
thismake these utilities lossy (last overload only, generics collapse). Know the limits before you build on them.
Done when you can
I can write
Exclude,ExtractandNonNullableand explain distribution.I can use
ReturnType,Parameters,ConstructorParametersandInstanceTypewithtypeof.I can write
ReturnTypeandParametersmyself withinfer.I can get the resolved data type of an async function with
Awaited.I can explain
OmitThisParameterand useNoInfer(5.4) to stop a position from widening a type parameter.