Conditional Types & infer
Conditional types bring if/else logic into the TypeScript type system. They let you compute types dynamically based on relationships between other types, unlocking a whole class of utility types that would otherwise be impossible to express.
Conditional Type Syntax
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"Read this as: "If T is assignable to string, produce 'yes', otherwise produce 'no'."
Distributive Conditional Types
When the checked type is a bare type parameter and you pass a union, the conditional type distributes over each member:
type ToArray<T> = T extends any ? T[] : never;
type Arr = ToArray<string | number>; // string[] | number[]To disable distribution, wrap the type parameter in a tuple:
type NoDistrib<T> = [T] extends [any] ? T[] : never;The infer Keyword
infer introduces a new type variable within the extends clause. TypeScript infers its value when the pattern matches:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R = ReturnType<() => string>; // stringBuilt-in Utility Types Using infer
TypeScript's own standard library uses infer extensively:
ReturnType<T>— return type of a functionParameters<T>— parameter tuple of a functionConstructorParameters<T>— constructor parameter tupleAwaited<T>— unwraps nested Promise types recursively
type Awaited<T> =
T extends null | undefined ? T :
T extends object & { then(onfulfilled: infer F): any } ?
F extends (value: infer V) => any ? Awaited<V> : never
: T;Building Custom Utility Types
You can compose conditional types to create powerful custom utilities:
type NonNullable<T> = T extends null | undefined ? never : T;
type Flatten<T> = T extends Array<infer Item> ? Item : T;Deep Readonly with Recursive Conditionals
Conditional types can be recursive, enabling transformations that descend into arbitrarily deep object structures.
Mastering conditional types and infer takes your TypeScript from merely type-safe to genuinely expressive — capable of encoding complex type-level algorithms that catch entire categories of bugs at compile time.
Code Examples
// IsNullable: true if T includes null or undefined, false otherwise
type IsNullable<T> = null extends T ? true : undefined extends T ? true : false;
type A = IsNullable<string | null>; // true
type B = IsNullable<string | undefined>; // true
type C = IsNullable<string>; // false
type D = IsNullable<number | null | undefined>; // true
// Unwrap nullable: strip null and undefined from a union
type NonNullableDeep<T> = T extends null | undefined ? never : T;
type E = NonNullableDeep<string | null | undefined>; // string
type F = NonNullableDeep<number | null>; // number
// Distributive conditional — maps over each union member
type Stringify<T> = T extends number ? `${T}` : T;
type G = Stringify<1 | 2 | "hello">; // "1" | "2" | "hello"
// Runtime validation helper leveraging the types above
function requireValue<T>(val: T): NonNullableDeep<T> {
if (val == null) throw new Error("Value is null or undefined");
return val as NonNullableDeep<T>;
}
console.log(requireValue("hello"));
console.log(requireValue(42));
try {
requireValue(null);
} catch (e) {
console.log((e as Error).message);
}IsNullable uses two nested conditional type checks to detect whether null or undefined is in the union. NonNullableDeep is a distributive conditional that removes those members. Stringify maps numeric literal types to their string equivalents.
// Manual implementation of ReturnType
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
// Manual implementation of Parameters
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
// Flatten an array one level deep
type Flatten<T> = T extends Array<infer Item> ? Item : T;
// Unwrap a Promise
type Awaited<T> = T extends Promise<infer V> ? Awaited<V> : T;
// --- Test cases ---
function fetchUser(id: string): Promise<{ name: string; age: number }> {
return Promise.resolve({ name: "Alice", age: 30 });
}
type FetchReturn = MyReturnType<typeof fetchUser>; // Promise<{name,age}>
type FetchParams = MyParameters<typeof fetchUser>; // [id: string]
type ResolvedUser = Awaited<FetchReturn>; // {name: string; age: number}
type NumberItem = Flatten<number[]>; // number
type PassThrough = Flatten<string>; // string (not an array)
// Verify at runtime
async function run() {
const user = await fetchUser("u-1");
console.log(`Name: ${user.name}, Age: ${user.age}`);
const params: FetchParams = ["u-2"];
console.log("Params tuple:", params);
}
run();MyReturnType uses infer R in the return position to capture whatever type the function returns. MyParameters uses infer P in the args position to capture the entire parameter tuple. Awaited recursively unwraps nested Promises.
type Primitive = string | number | boolean | bigint | symbol | null | undefined;
type DeepReadonly<T> =
T extends Primitive
? T
: T extends Array<infer Item>
? ReadonlyArray<DeepReadonly<Item>>
: T extends Map<infer K, infer V>
? ReadonlyMap<K, DeepReadonly<V>>
: T extends Set<infer Item>
? ReadonlySet<DeepReadonly<Item>>
: { readonly [K in keyof T]: DeepReadonly<T[K]> };
// Test with a nested structure
interface AppConfig {
server: {
host: string;
port: number;
tls: { enabled: boolean; cert: string };
};
features: string[];
limits: Map<string, number>;
}
type FrozenConfig = DeepReadonly<AppConfig>;
// FrozenConfig.server.host → readonly string
// FrozenConfig.server.tls.enabled → readonly boolean
// FrozenConfig.features → ReadonlyArray<string>
// FrozenConfig.limits → ReadonlyMap<string, number>
const config: FrozenConfig = {
server: { host: "localhost", port: 3000, tls: { enabled: true, cert: "..." } },
features: ["auth", "logging"],
limits: new Map([["rps", 1000]]),
};
console.log(config.server.host);
console.log(config.server.tls.enabled);
console.log(config.features[0]);
console.log(config.limits.get("rps"));DeepReadonly recurses through primitives, arrays, Maps, Sets, and plain objects, applying readonly at every level. The infer keyword extracts element/key/value types from generic containers so they can be recursively wrapped.
Quick Quiz
1. What does the infer keyword do in a conditional type?
2. What is a distributive conditional type?
3. What does type Flatten<T> = T extends Array<infer Item> ? Item : T evaluate to for Flatten<number[]>?
4. How do you prevent a conditional type from distributing over a union?
Was this lesson helpful?