Conditional Types¶
A conditional type looks like this:
T extends U ? X : Y
It reads like:
“If
Tis assignable toU, then returnX, otherwise returnY.”
This is TypeScript’s version of an if/else at the type level.
Basic Example¶
type IsString<T> = T extends string ? true : false
Usage:
type A = IsString<string> // true
type B = IsString<number> // false
This is the foundation for more advanced patterns.
Conditional Types with Unions¶
Conditional types distribute over unions.
type IsString<T> = T extends string ? true : false
type Result = IsString<string | number>
// true | false
This is called distributive conditional types, and it’s incredibly powerful.
Filtering Unions¶
You can use conditional types to filter unions.
Example: Keep only strings¶
type OnlyStrings<T> = T extends string ? T : never
Usage:
type Mixed = string | number | boolean
type StringsOnly = OnlyStrings<Mixed>
// string
Example: Remove strings¶
type WithoutStrings<T> = T extends string ? never : T
type NoStrings = WithoutStrings<Mixed>
// number | boolean
This is how Exclude<T, U> is implemented.
Conditional Types with Functions¶
You can extract information from function types.
Example: Extract return type¶
type Return<T> = T extends (...args: any[]) => infer R ? R : never
Usage:
function getUser() {
return { id: 1, name: "Alice" }
}
type User = Return<typeof getUser>
// { id: number; name: string }
This is exactly how ReturnType<T> works.
Inferring Types with infer¶
infer lets you capture a type inside a conditional type.
It’s like saying:
“If this pattern matches, extract the type inside and call it
R.”
Example: Extract function argument type¶
type FirstArg<T> =
T extends (arg: infer A, ...rest: any[]) => any
? A
: never
Usage:
type A = FirstArg<(x: number, y: string) => void>
// number
Example: Extract array element type¶
type Element<T> = T extends (infer U)[] ? U : never
Usage:
type E = Element<string[]> // string
This is how Awaited<T> and many array utilities work.
Example: Extract Promise result¶
type UnwrapPromise<T> =
T extends Promise<infer R> ? R : T
Usage:
type A = UnwrapPromise<Promise<number>> // number
type B = UnwrapPromise<string> // string
This is the foundation of async type utilities.
Real‑World Example: Deep Readonly¶
Using conditional types + mapped types + infer:
type DeepReadonly<T> =
T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T
Usage:
type User = {
id: number
profile: {
name: string
email: string
}
}
type Frozen = DeepReadonly<User>
Result:
- all nested properties become readonly
- works recursively
- no runtime cost
This is how advanced libraries implement deep transformations.
Real‑World Example: Extracting Event Payloads¶
Imagine an event map:
type Events = {
login: { userId: string }
logout: { userId: string }
error: { message: string }
}
We can extract payload types:
type EventPayload<E extends keyof Events> = Events[E]
But we can also infer event names from payloads:
type EventNameFromPayload<P> =
{ [K in keyof Events]: Events[K] extends P ? K : never }[keyof Events]
Usage:
type Name = EventNameFromPayload<{ userId: string }>
// "login" | "logout"
Conditional types let you build dynamic, type‑safe event systems.
Putting It All Together¶
Here’s a powerful example combining everything:
type AsyncResult<T> =
T extends Promise<infer R>
? { type: "promise"; value: R }
: T extends (...args: any[]) => infer F
? { type: "function"; value: F }
: { type: "other"; value: T }
Usage:
type A = AsyncResult<Promise<number>>
// { type: "promise"; value: number }
type B = AsyncResult<() => string>
// { type: "function"; value: string }
type C = AsyncResult<boolean>
// { type: "other"; value: boolean }
This is the kind of expressive type logic that makes TypeScript so powerful.
Summary¶
In this lesson, you learned how conditional types enable type‑level logic:
1. T extends U ? X : Y¶
- type‑level branching
- filtering unions
- transforming types
2. infer¶
- extract return types
- extract argument types
- unwrap arrays and promises
- build advanced utilities
Conditional types + infer are the backbone of TypeScript’s advanced type system.
They let you write types that compute, transform, and adapt—just like real code.