TypeScript course menu
TypeScript Conditional Types and infer
Learn TypeScript conditional types: the T extends U ? X : Y syntax, distribution over unions, how Exclude, Extract and NonNullable work, extracting types with infer, unwrapping arrays and promises, and keeping conditional types readable.
On this page
A conditional type chooses between two types based on a condition, like a ternary operator for types:
T extends U ? X : Y"If T is assignable to U, the type is X; otherwise it is Y." Conditional types are mostly used inside generic types to compute one type from another.
A first example#
type IsString<T> = T extends string ? "yes" : "no";
const a: IsString<"hello"> = "yes";
const b: IsString<42> = "no";
console.log(a, b);yes notype IsString<T> = T extends string ? "yes" : "no";
const c: IsString<number> = "yes";error TS2322: Type '"yes"' is not assignable to type '"no"'.A practical example#
Return a different type depending on the input type:
type Wrapped<T> = T extends unknown[] ? T : T[];
function toArray<T>(value: T): Wrapped<T> {
return (Array.isArray(value) ? value : [value]) as Wrapped<T>;
}
const one = toArray("tea"); // string[]
const many = toArray([1, 2]); // number[]
console.log(one, many);[ 'tea' ] [ 1, 2 ]The implementation needs a type assertion (as Wrapped<T>), because TypeScript cannot follow a runtime check into an unresolved conditional type. This is common when implementing functions with conditional return types.
Distribution over unions#
When the checked type is a bare type parameter and you pass a union, the condition is applied to each member separately, and the results are combined:
type ToArray<T> = T extends unknown ? T[] : never;
type R = ToArray<string | number>; // string[] | number[] (not (string | number)[])
const r1: R = ["a", "b"];
const r2: R = [1, 2];
console.log(r1.length + r2.length);4This is exactly how the built-in Exclude and Extract work:
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type Status = "draft" | "review" | "published";
type Live = MyExclude<Status, "draft">; // "review" | "published"
type OnlyDraft = MyExtract<Status, "draft">; // "draft"
const live: Live = "published";
const draft: OnlyDraft = "draft";
console.log(live, draft);published draftReturning never for a member removes it from the resulting union. To turn distribution off, wrap both sides in brackets: [T] extends [U] ? X : Y.
infer: extracting a type#
Inside the condition, infer declares a type variable that TypeScript fills in by matching:
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Unpromise<T> = T extends Promise<infer V> ? V : T;
type FirstArg<F> = F extends (first: infer A, ...rest: never[]) => unknown ? A : never;
type E = ElementOf<string[]>; // string
type V = Unpromise<Promise<{ id: number }>>; // { id: number }
type A = FirstArg<(name: string, age: number) => void>; // string
const e: E = "item";
const v: V = { id: 1 };
const arg: A = "Asha";
console.log(e, v.id, arg);item 1 AshaRead T extends Promise<infer V> ? V : T as: "if T is a promise of something, call that something V and return it; otherwise return T". The built-in ReturnType, Parameters and Awaited are written this way.
Recursive conditional types#
Conditional types can refer to themselves, for example to unwrap nested arrays completely:
type DeepElement<T> = T extends readonly (infer E)[] ? DeepElement<E> : T;
type Deep = DeepElement<number[][][]>; // number
const d: Deep = 42;
console.log(d);42Keep them readable#
Conditional types are powerful, but heavy type-level code is hard to read and slow down the compiler. Prefer the built-in utility types where possible, give intermediate types descriptive names, and add a short comment explaining what a complex type computes. Most application code needs only a few simple conditional types; libraries use them more.
Practice#
Practice exercise
Write type ApiData<T> = T extends { data: infer D } ? D : never, and use it to extract the data type from type UserResponse = { status: number; data: { id: number; name: string } }. Declare a value of the extracted type and print it.
Show solution
type ApiData<T> = T extends { data: infer D } ? D : never;
type UserResponse = { status: number; data: { id: number; name: string } };
type UserData = ApiData<UserResponse>; // { id: number; name: string }
const user: UserData = { id: 7, name: "Meera" };
console.log(user);{ id: 7, name: 'Meera' }Key takeaway#
Finished this lesson?