TypeScript Conditional Types — Complete Guide
In this tutorial, you will learn about TypeScript Conditional Types. We cover key concepts, practical examples, and best practices to help you master this topic.
TypeScript conditional types select one type or another based on a condition — like an if/else for types — enabling powerful Metaprogramming patterns where types adapt based on input, extract inner types, and filter union members.
What You'll Learn
- Conditional type syntax:
T extends U ? X : Y - The
inferkeyword for type extraction - Distributive conditional types over unions
- Recursive conditional types
- Practical patterns: type filtering, unwrapping
Why It Matters
Conditional types are the foundation of TypeScript's utility types (ReturnType, Extract, NonNullable) and generic constraints. They let you build types that react to their inputs — making the type system programmable rather than declarative.
Real-World Use
Prisma's generated types use conditional types extensively — optional relations become User | null while required relations are User. The Parameters and ReturnType utilities (built with conditional types + infer) are used by Durga Antivirus Pro's API client to automatically derive types from endpoint handler functions.
Learning Path
flowchart LR A[Template Literal Types] --> B[Conditional Types] B --> C[Mapped Types] B --> D[You Are Here] C --> E[Utility Types] E --> F[Namespaces & Modules]
Basic Conditional Types
type IsNumber<T> = T extends number ? "yes" : "no";
type A = IsNumber<42>; // "yes"
type B = IsNumber<string>; // "no"
type C = IsNumber<number>; // "yes"
Think of it as a ternary for types: "If T extends (matches) U, use type X, otherwise use type Y."
Nested Conditionals
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type A = TypeName<string>; // "string"
type B = TypeName<[1, 2]>; // "object" (arrays are objects)
type C = TypeName<true>; // "boolean"
The infer Keyword
infer lets you declare a type variable within the extends clause and capture a part of the matched type:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn1 = () => string;
type Fn2 = (x: number) => boolean;
type R1 = ReturnType<Fn1>; // string
type R2 = ReturnType<Fn2>; // boolean
Multiple infer Positions
type SwapPair<T> = T extends [infer A, infer B] ? [B, A] : T;
type Swapped = SwapPair<["hello", 42]>;
// [number, string] (positions swapped)
type Params<T> = T extends (...args: infer P) => any ? P : never;
type FnParams = Params<(name: string, age: number) => void>;
// [string, number]
Deep Infer
type UnwrapPromise<T> = T extends Promise<infer U>
? U extends Promise<infer V>
? V
: U
: T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<Promise<number>>>; // number
type C = UnwrapPromise<number>; // number
Distributive Conditional Types
When a conditional type is used on a bare generic type parameter, it distributes over unions:
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// = ToArray<string> | ToArray<number>
// = string[] | number[]
// Without distribution — wrap in [T]
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// = (string | number)[]
Filtering Union Members
type ExcludeNull<T> = T extends null | undefined ? never : T;
type Values = string | null | boolean | undefined;
type NonNullValues = ExcludeNull<Values>;
// string | boolean
This is how TypeScript's built-in Exclude<T, U> works:
type MyExclude<T, U> = T extends U ? never : T;
type Result = MyExclude<"a" | "b" | "c", "a">; // "b" | "c"
Extract
type MyExtract<T, U> = T extends U ? T : never;
type Result = MyExtract<"a" | "b" | "c", "a" | "b">;
// "a" | "b"
Practical Conditional Type Patterns
Nullable Detection
type IsNullable<T> = T extends null | undefined ? true : false;
type A = IsNullable<string>; // false
type B = IsNullable<string | null>; // true
type C = IsNullable<undefined>; // true
Function Check
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;
type A = IsFunction<() => void>; // true
type B = IsFunction<string>; // false
Array Element Extraction
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<number>; // number
JSON Types
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
type JSONType<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends null ? "null" :
T extends JSONValue[] ? "array" :
T extends object ? "object" :
"unknown";
Recursive Conditional Types
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
interface Config {
name: string;
nested: {
port: number;
deep: {
flag: boolean;
};
};
}
type ReadonlyConfig = DeepReadonly<Config>;
// All properties and nested properties are readonly
Common Mistakes
1. Forgetting Distribution
type IsString<T> = T extends string ? true : false;
type Result = IsString<string | number>; // boolean (true | false), not false!
Wrap in [T] to prevent distribution if that's not what you want.
2. Not Handling the never Case
type Filter<T> = T extends string ? T : never;
type Result = Filter<string | number>; // string only
// But what about Filter<never>? Returns never.
// In distributed conditionals over empty union, the result is never.
3. Using infer in a Non-Conditional Position
// Error: infer is only valid in extends clause
// type Bad<T> = T extends infer U ? U : never; // OK
// type Bad2<T> = infer U; // Error
4. Recursive Conditionals Without a Base Case
Always ensure there's a base case that doesn't recurse:
type Flatten<T> = T extends (infer U)[]
? U extends (infer V)[] // Recursive
? Flatten<V>
: U // Base case: non-array element
: T; // Base case: not an array
5. Forgetting to Test with Union Types
Conditional types behave differently with unions (distribution). Always test with union inputs.
6. Over-Nesting Conditionals
// Hard to read
type Complex<T> =
T extends A ? "a" :
T extends B ? "b" :
T extends C ? "c" :
T extends D ? "d" : "other";
// Better: use helper types
type IsA<T> = T extends A ? true : false;
type IsB<T> = T extends B ? true : false;
Practice Questions
What is the syntax for a conditional type?
T extends U ? X : Y— if T extends (is assignable to) U, the type is X, otherwise Y.What does the
inferkeyword do? It declares a type variable inside the extends clause that captures a part of the matched type for use in the true branch.What is distributive conditional typing? When a conditional type on a bare generic distributes over each member of a union, applying the condition individually and unioning the results.
How do you prevent distribution in a conditional type? Wrap both sides of extends in tuples:
[T] extends [U] ? X : Y.
Challenge: Write a FunctionPropertyNames<T> conditional type that returns the keys of T whose values are functions. Use it to extract only the method names from an interface.
FAQ
Mini Project: Type-Safe API Client
// src/api-client.ts
type APIMethods = "GET" | "POST" | "PUT" | "DELETE";
type Endpoints = {
"/users": { GET: User[]; POST: UserCreate };
"/users/:id": { GET: User; PUT: UserUpdate; DELETE: void };
"/scans": { GET: Scan[]; POST: ScanRequest };
};
type ExtractResponse<T, M extends APIMethods> =
T extends { [K in M]: infer R } ? R : never;
type RequestBody<T, M extends APIMethods> =
M extends "POST" | "PUT"
? T extends { [K in M]: infer R } ? R : never
: undefined;
class APIClient {
async request<E extends keyof Endpoints, M extends APIMethods>(
endpoint: E,
method: M,
body?: RequestBody<Endpoints[E], M>
): Promise<ExtractResponse<Endpoints[E], M>> {
const response = await fetch(endpoint as string, {
method,
body: body ? JSON.stringify(body) : undefined,
});
return response.json();
}
}
interface User { id: string; name: string; }
interface Scan { id: string; threats: string[]; }
const client = new APIClient();
const users = await client.request("/users", "GET");
// type: User[]
const scan = await client.request("/scans", "POST", { /* type-checked */ });
What's Next
Now explore mapped types for transforming object types:
| Lesson | Description |
|---|---|
| {{< ref "/programming-languages/typescript/12-template-literal-types" >}} | Review template literal types |
| {{< ref "/programming-languages/typescript/14-mapped-types" >}} | Partial, Required, Readonly, Pick, Record |
| {{< ref "/programming-languages/typescript/15-utility-types" >}} | Omit, Extract, Exclude, NonNullable, ReturnType |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro