Conditional Types in TypeScript
Conditional types are one of TypeScript’s most powerful type-system features. They allow you to create types that adapt based on other types, enabling truly generic and reusable type transformations. In this tutorial, you’ll learn how to harness conditional types to write more expressive and flexible TypeScript code.
Basic Conditional Type Syntax
A conditional type selects one of two types based on a type relationship:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
The syntax reads like: “If T extends string, then the type is true, otherwise it’s false.”
This becomes powerful when combined with generics:
type Nullable<T> = T extends null | undefined ? never : T;
type A = Nullable<string>; // string
type B = Nullable<null>; // never
type C = Nullable<string | null>; // string
The infer Keyword
The infer keyword lets you extract types from within other types—it’s like pattern matching for TypeScript:
// Extract the return type from a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: "Alice" };
}
type User = ReturnType<typeof getUser>;
// { id: number; name: string }
Here’s how infer works inside a conditional type:
// Extract the first element from an array
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type A = First<[string, number, boolean]>; // string
type B = First<[]>; // never
Practical Conditional Types
Let’s build some useful conditional types from scratch:
Filter Out Nullish Types
type NonNullable<T> = T extends null | undefined ? never : T;
type Clean = NonNullable<string | null | undefined | number>;
// string | number
Extract Function Parameters
type Parameters<T extends (...args: any[]) => any> =
T extends (...args: infer P) => any ? P : never;
function createUser(name: string, age: number, email: string) {
return { name, age, email };
}
type Params = Parameters<typeof createUser>;
// [string, number, string]
Check if a Type Is Any
type IsAny<T> = 0 extends (1 & T) ? true : false;
type A = IsAny<any>; // true
type B = IsAny<string>; // false
type C = IsAny<never>; // false
This works because any is the only type that can intersect with anything and produce 0.
Mapped Conditionals
Combine conditional types with mapped types for powerful transformations:
type MakeOptional<T> = {
[K in keyof T]?: T[K];
};
type MakeRequired<T> = {
[K in keyof T]-?: T[K];
};
interface User {
id: number;
name: string;
email: string;
}
type OptionalUser = MakeOptional<User>;
type RequiredUser = MakeRequired<OptionalUser>; // Back to original
Conditional Property Types
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Config {
api: {
url: string;
headers: Record<string, string>;
};
maxRetries: number;
}
type FrozenConfig = DeepReadonly<Config>;
// {
// readonly api: {
// readonly url: string;
// readonly headers: Readonly<Record<string, string>>;
// };
// readonly maxRetries: number;
// }
Distributive Conditional Types
When the checked type is a union, conditional types become distributive:
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>;
// string[] | number[]
This is why Exclude and Extract work:
// Exclude: remove T from U
type Exclude<T, U> = T extends U ? never : T;
// Extract: keep only T from U
type Extract<T, U> = T extends U ? T : never;
type A = Exclude<string | number, string>; // number
type B = Extract<string | number, string>; // string
Real-World Example: API Response Types
Here’s a practical example combining everything:
// Define API response shapes
interface ApiSuccess<T> {
ok: true;
data: T;
}
interface ApiError {
ok: false;
error: {
code: number;
message: string;
};
}
type ApiResponse<T> = ApiSuccess<T> | ApiError;
// Type guard to narrow the response
function isSuccess<T>(response: ApiResponse<T>): response is ApiSuccess<T> {
return response.ok === true;
}
// Usage
async function fetchUser(id: number): Promise<ApiResponse<{ id: number; name: string }>> {
try {
const response = await fetch(`/api/users/${id}`);
return { ok: true, data: await response.json() };
} catch {
return { ok: false, error: { code: 500, message: "Network error" } };
}
}
// Narrowing the type
const result = await fetchUser(1);
if (isSuccess(result)) {
console.log(result.data.name); // TypeScript knows data exists
} else {
console.error(result.error.message); // TypeScript knows error exists
}
Built-in Conditional Types
TypeScript provides several built-in conditional types in the standard library:
| Type | Description |
|---|---|
ReturnType<T> | Extracts the return type of a function |
Parameters<T> | Extracts the parameter types as a tuple |
InstanceType<T> | Extracts the instance type of a class |
Exclude<T, U> | Excludes types from T that are assignable to U |
Extract<T, U> | Extracts types from T that are assignable to U |
NonNullable<T> | Removes null and undefined from T |
Summary
Conditional types are essential for advanced TypeScript:
- Basic syntax:
T extends U ? X : Ychooses types based on relationships infer: Extracts types from within other types- Distribution: Union types distribute across conditional types
- Practical applications: Build utilities like
Nullable,Optional, andReturnType
Master conditional types, and you’ll unlock TypeScript’s full type-system potential. In the next tutorial, you’ll explore more advanced type patterns that build on these concepts.