TypeScript 31 ๐ท Conditional Types
A conditional type is TypeScript’s if statement at the type level. It reads T extends U ? X : Y โ “if T is assignable to U, use type X; otherwise use type Y.” Conditional types let you write type-level functions that choose a result based on an input type. They’re the mechanism behind Exclude, Extract, NonNullable, ReturnType, Parameters, and most of the advanced types you’ll encounter in libraries. Once you understand them, the utility types stop being black boxes and become patterns you can extend.
Key point: A conditional type is T extends U ? X : Y. The extends here means assignability โ same as in constraints, not inheritance. Conditional types are distributive over unions: when T is a union, the condition is applied to each member separately, and the results are combined. That distribution is powerful and surprising โ and the source of half the gotchas. When you want to avoid distribution, wrap both sides in [T] extends [U].
What a conditional type is
The syntax is T extends U ? X : Y โ a question and two branches.
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<string>; // 'yes'
type B = IsString<number>; // 'no'
type C = IsString<'hello'>; // 'yes'
IsString<T> reads: “if T is assignable to string, the type is 'yes'; otherwise 'no'.”
The three parts:
T extends Uโ the conditionXโ the result when trueYโ the result when false
A practical example:
type ElementType<T> = T extends (infer U)[] ? U : never;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<{ x: 1 }>; // never
ElementType<T> says: “if T is an array type, extract the element type; otherwise never.” The infer U captures the element type of the array.
Why conditional types exist: They let you write types that depend on other types โ a form of pattern matching at the type level. Without them, you couldn’t express “if this is an array, give me its element type” or “if this is a function, give me its return type.” They’re the foundation of TypeScript’s type-level programming.
Conditional types vs generics: A generic is a placeholder for a type. A conditional type is a choice between types. They combine โ a generic function with a conditional return type is a type-level function.
Why “extends” and not “is”: The same keyword as constraints and inheritance. In all three, it means “assignable to” or “a subset of.” Consistency matters more than picking the perfect word. Once you internalize “extends = assignable,” the syntax reads naturally.
Distribution over unions
When the condition’s left side is a union, TypeScript distributes the conditional over each member.
type ToArray<T> = T extends unknown ? T[] : never;
type A = ToArray<string | number>;
// string[] | number[]
The union string | number is split into string and number. Each is checked separately. The results are combined.
Without distribution โ the surprise:
type NonString<T> = T extends string ? never : T;
type A = NonString<string | number>; // number
type B = NonString<string>; // never
type C = NonString<number>; // number
string | number distributes. For string, the result is never. For number, the result is number. Combining: never | number = number. So NonString<string | number> is number โ the string was filtered out.
The never case: Distribution over never produces never โ the empty union distributes to nothing.
type A = NonString<never>; // never
Why distribution matters: It’s how types like Exclude work.
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<'a' | 'b' | 'c', 'b'>;
// 'a' | 'c'
Exclude distributes over the union 'a' | 'b' | 'c'. Each member is checked against 'b'. 'a' isn’t assignable to 'b' โ kept. 'b' is assignable โ becomes never. 'c' isn’t โ kept. Result: 'a' | 'c'.
Preventing distribution โ [T] extends [U]:
type IsString<T> = [T] extends [string] ? true : false;
type A = IsString<string | number>;
// false โ the whole union isn't assignable to string
Wrapping both sides in tuples turns off distribution. Now T is checked as a whole.
Why the tuple trick works: Tuples aren’t naked type parameters โ they’re composite types. Distribution only applies to bare type parameters on the left of extends. [T] is a tuple containing T, so the whole thing is checked as one unit.
When to prevent distribution:
- Checking if a union as a whole matches
- Testing exact equality of types
- Building non-distributive filters
Why distribution exists: It makes
Exclude,Extract, andNonNullablework โ filtering a union by removing members that match. Without distribution, filtering unions would need an entirely different mechanism. Distribution is the feature; sometimes you have to turn it off deliberately.
infer โ extract a type
The infer keyword declares a type variable inside a conditional type โ capturing the type at that position.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type A = ReturnType<() => string>; // string
type B = ReturnType<(x: number) => boolean>; // boolean
infer R says: “if T is a function, bind its return type to R and use it.” The R is captured from the function type.
Multiple infer:
type First<T> = T extends [infer F, ...unknown[]] ? F : never;
type A = First<[1, 2, 3]>; // 1
type B = First<['a', 'b']>; // 'a'
type C = First<[]>; // never
infer F captures the first element of a tuple.
infer in different positions:
// Function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
// Promise value
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
// Array element
type Flatten<T> = T extends Array<infer U> ? U : T;
// Object value
type ObjectValue<T> = T extends Record<string, infer V> ? V : never;
// Constructor instance
type InstanceType<T> = T extends new (...args: any[]) => infer R ? R : never;
Each extracts a different part of the structure.
infer in the true branch only: infer can only appear in the true branch of the conditional.
// โ
Valid
type F<T> = T extends (...args: any[]) => infer R ? R : never;
// โ Invalid
type G<T> = T extends (...args: any[]) => any ? infer R : never;
The infer must be part of the pattern being matched.
infer with constraints: In TypeScript 4.7+, infer can have its own constraint.
type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
type A = FirstString<['hello', 1]>; // 'hello'
type B = FirstString<[1, 2]>; // never
infer S extends string requires the captured type to be a string.
Why infer matters: It’s how you destructure types. Without it, you can match a type but can’t extract its parts. infer gives you variables to capture them. Every utility type that extracts something โ ReturnType, Parameters, Awaited, InstanceType โ uses infer.
Why
inferin the true branch only: The pattern being matched is on the left ofextends.inferintroduces a variable that captures a part of that pattern. It only makes sense when the pattern matched โ hence, only in the true branch. In the false branch, there’s no match, so nothing to capture.
Utility types built on conditional types
TypeScript’s standard library has several conditional types. Understanding them means understanding the pattern.
Exclude<T, U> โ remove members of U from T:
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<'a' | 'b' | 'c', 'b'>; // 'a' | 'c'
type B = Exclude<string | number, string>; // number
Distributes over T, removing any member assignable to U.
Extract<T, U> โ keep members of T that are assignable to U:
type Extract<T, U> = T extends U ? T : never;
type A = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'
type B = Extract<string | number, number>; // number
The complement of Exclude.
NonNullable<T> โ remove null and undefined:
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>; // string
type B = NonNullable<string | null | undefined>; // string
Distribution removes the null and undefined branches.
ReturnType<T> โ function return type:
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
type A = ReturnType<() => string>; // string
type B = ReturnType<(x: number) => void>; // void
Parameters<T> โ function parameter types:
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type A = Parameters<(x: number, y: string) => void>;
// [x: number, y: string]
Awaited<T> โ unwrap Promise:
type Awaited<T> =
T extends Promise<infer U> ? Awaited<U> : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
Recursive โ unwraps nested promises.
InstanceType<T> โ instance of constructor:
type InstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer R ? R : never;
class User {}
type U = InstanceType<typeof User>; // User
Why these are the same pattern: All extract something from a type. Exclude and Extract filter a union. ReturnType and Parameters destructure a function. Awaited unwraps a promise. Each is a small conditional type with infer or a simple branch.
Why learning them pays off: They’re the vocabulary of TypeScript’s type system. When you see
ReturnType<T>in a library, knowing it’sT extends (...args) => infer R ? R : nevertells you exactly what it does. The standard utilities aren’t magic โ they’re the same pattern you can write yourself.
Nested conditionals
Conditional types can be chained to check multiple cases.
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<number>; // 'number'
type C = TypeName<() => void>; // 'function'
type D = TypeName<{ x: 1 }>; // 'object'
Each condition is checked in order. The first match wins.
A more complex example โ flattening an array one level:
type Flatten<T> =
T extends Array<infer U> ? U :
T extends ReadonlyArray<infer U> ? U :
T;
type A = Flatten<string[]>; // string
type B = Flatten<readonly number[]>; // number
type C = Flatten<boolean>; // boolean
The first condition matches mutable arrays; the second matches readonly arrays; otherwise, the type is unchanged.
Recursive conditionals: A conditional type can reference itself.
type DeepReadonly<T> =
T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface Nested {
user: { name: string; address: { city: string } };
count: number;
}
type Frozen = DeepReadonly<Nested>;
// All levels readonly
The DeepReadonly condition recurses: if T is an object, wrap it recursively; otherwise, leave it alone. TypeScript’s type system resolves this lazily, terminating when the condition is false.
Why nesting works: Each branch is another conditional type. TypeScript evaluates them in order, and only the matching branch’s result matters. Recursion happens because a conditional type can reference itself in its own result.
When nesting gets unwieldy: Deeply chained conditionals are hard to read. The TypeName example is about the limit. Past that, split into named types.
Why recursion terminates: TypeScript resolves conditional types lazily. When it hits a case where the condition is false, the recursion stops. Without that, recursive types would loop forever. TypeScript also has limits on recursion depth โ deep enough types hit an error rather than hanging.
Practical patterns
Conditional types solve real problems.
Extract a property type:
type PropType<T, K extends keyof T> = T[K];
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
Not conditional, but shows the simpler case.
Unwrap a Promise:
async function fetchValue<T>(url: string): Promise<T> {
const res = await fetch(url);
return res.json() as T;
}
type FetchResult = Awaited<ReturnType<typeof fetchValue<number>>>;
// number
Test if a type is an array:
type IsArray<T> = T extends unknown[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<string>; // false
Test if two types are equal:
type Equals<A, B> =
(<T>() => T extends A ? 1 : 2) extends
(<T>() => T extends B ? 1 : 2)
? true
: false;
type A = Equals<string, string>; // true
type B = Equals<string, number>; // false
The trick: two conditional types are “the same” only if A and B are identical. The <T>() => T extends X ? 1 : 2 pattern captures the conditional as a value; comparing those values works for exact equality.
Extract keys of a specific value type:
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
interface Mixed {
id: number;
name: string;
active: boolean;
count: number;
}
type NumberKeys = KeysOfType<Mixed, number>;
// 'id' | 'count'
type StringKeys = KeysOfType<Mixed, string>;
// 'name'
The mapped type checks each value against V, producing the key or never. Indexing with [keyof T] gives the union.
Get the union of a specific property:
type ValuesOfType<T, V> = {
[K in keyof T]: T[K] extends V ? T[K] : never;
}[keyof T];
type Numbers = ValuesOfType<Mixed, number>; // number
Why these patterns recur: They’re the type-level equivalent of filters and maps. KeysOfType filters keys by value type. Equals checks identity. Awaited unwraps a promise. Each is a small conditional type that solves a real problem.
Why
Equalsis subtle: TypeScript’s assignability is structural โ{ a: 1 }and{ a: 1 }are “the same.” But sometimes you want exact equality, including distinctions likeanyvsunknown.Equalsuses a trick with function types to detect exact identity. It’s used in type-level testing and conditional branches that must distinguish otherwise-similar types.
infer constraints
TypeScript 4.7 added infer X extends Y โ constraining the inferred type.
type FirstNumber<T> =
T extends [infer N extends number, ...unknown[]] ? N : never;
type A = FirstNumber<[1, 'a']>; // 1
type B = FirstNumber<['a', 1]>; // never
infer N extends number requires the captured type to be assignable to number. If not, the match fails.
Without the constraint, infer captures anything:
type FirstUnconstrained<T> =
T extends [infer F, ...unknown[]] ? F : never;
type A = FirstUnconstrained<[1, 'a']>; // 1
type B = FirstUnconstrained<['a', 1]>; // 'a'
No constraint โ any type can be captured.
With the constraint:
type FirstNumeric<T> =
T extends [infer N extends number, ...unknown[]] ? N : never;
type A = FirstNumeric<[1, 'a']>; // 1
type B = FirstNumeric<['a', 1]>; // never
The second case fails because 'a' isn’t assignable to number.
Practical use โ extracting a specific kind of value:
type ExtractStrings<T> =
T extends (infer S extends string)[] ? S[] : never;
type A = ExtractStrings<['a', 'b']>; // ['a', 'b'] โ 'a' | 'b'[]
type B = ExtractStrings<[1, 2]>; // never
Only arrays of strings are matched.
Why constrained infer matters: It filters during capture. Instead of capturing everything and filtering later, you reject non-matching cases immediately. Fewer branches, simpler logic.
Why
infer extendswas added late: Before 4.7, you’d have to capture withinferand then check the type in a nested conditional. The constraint collapsed that into one step. It’s sugar over the same behavior โ but it removes a common source of repetition.
A full example
A small type-level library using conditional types.
// ============================================
// UTILITY TYPES
// ============================================
// Get the element type of an array
type ElementOf<T> = T extends (infer U)[] ? U : never;
// Get the value type of a Promise
type Unwrap<T> = T extends Promise<infer U> ? U : T;
// Unwrap nested promises
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
// Check if a type is a function
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;
// Get the return type of a function, or the type itself
type ReturnOrSelf<T> =
T extends (...args: any[]) => infer R ? R : T;
// Extract keys of a specific value type
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
// Get a property type safely
type SafeProp<T, K> = K extends keyof T ? T[K] : never;
// ============================================
// APPLYING THEM
// ============================================
interface User {
id: number;
name: string;
email: string;
active: boolean;
}
type NumberKeys = KeysOfType<User, number>;
// 'id'
type StringKeys = KeysOfType<User, string>;
// 'name' | 'email'
type UserId = SafeProp<User, 'id'>;
// number
type Nonexistent = SafeProp<User, 'missing'>;
// never
// ============================================
// FUNCTION TYPES
// ============================================
function fetchUser(id: number): Promise<User> {
return fetch(`/users/${id}`).then(r => r.json());
}
type FetchUserReturn = ReturnType<typeof fetchUser>;
// Promise<User>
type FetchUserData = DeepUnwrap<FetchUserReturn>;
// User
type IsFetchFunction = IsFunction<typeof fetchUser>;
// true
type IsUserFunction = IsFunction<User>;
// false
// ============================================
// ARRAY TYPES
// ============================================
type NamesArray = string[];
type AgeArray = number[];
type NameElement = ElementOf<NamesArray>;
// string
type AgeElement = ElementOf<AgeArray>;
// number
type NotAnArray = ElementOf<User>;
// never
// ============================================
// CONDITIONAL CHAINS
// ============================================
type Describe<T> =
T extends string ? `string: ${T}` :
T extends number ? `number: ${T}` :
T extends boolean ? `boolean: ${T}` :
T extends Function ? 'function' :
T extends object ? 'object' :
'unknown';
type D1 = Describe<string>; // `string: string`
type D2 = Describe<42>; // `number: 42`
type D3 = Describe<true>; // 'boolean: true'
type D4 = Describe<() => void>; // 'function'
type D5 = Describe<User>; // 'object'
// ============================================
// USAGE
// ============================================
const userKeys: StringKeys[] = ['name', 'email'];
const idKeys: NumberKeys[] = ['id'];
console.log(userKeys, idKeys);
function getField<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
active: true
};
const id = getField(user, 'id'); // number
const name = getField(user, 'name'); // string
What this shows:
ElementOfโ extracts an array’s element type viainferUnwrapandDeepUnwrapโ unwrap promises, recursivelyIsFunctionโ tests if a type is callableReturnOrSelfโ returns the function’s return type or the type itselfKeysOfTypeโ conditional + mapped + indexed accessSafePropโ safe property access withneverfor missing keysDescribeโ a chain of conditionals producing different labels
Each type is a conditional in one form or another. Combined, they form a small type-level toolkit.
Why this shape: It’s how real type-level utilities are written. Extract, test, filter, chain. The building blocks โ
infer, distribution,neverfiltering, nesting โ combine into everything fromReturnTypetoKeysOfType. Once you can write these, you can read any library’s utility types.
Complete Example Session
# ============================================
# PART 1: BASIC CONDITIONAL
# ============================================
cat > basic.ts << 'EOF'
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<string>;
type B = IsString<number>;
type C = IsString<'hello'>;
const a: A = 'yes';
const b: B = 'no';
const c: C = 'yes';
console.log(a, b, c);
EOF
npx tsc --noEmit basic.ts
# (no errors)
# ============================================
# PART 2: DISTRIBUTION
# ============================================
cat > distribution.ts << 'EOF'
type ToArray<T> = T extends unknown ? T[] : never;
type A = ToArray<string | number>;
// string[] | number[]
const a: A = ['hello'];
const b: A = [42];
// With tuple wrap โ no distribution
type ToArray2<T> = [T] extends [unknown] ? T[] : never;
type B = ToArray2<string | number>;
// (string | number)[]
const c: B = ['hello', 42];
console.log(a, b, c);
EOF
npx tsc --noEmit distribution.ts
# (no errors)
# ============================================
# PART 3: EXCLUDE AND EXTRACT
# ============================================
cat > exclude.ts << 'EOF'
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type A = MyExclude<'a' | 'b' | 'c', 'b'>;
// 'a' | 'c'
type B = MyExtract<'a' | 'b' | 'c', 'a' | 'b'>;
// 'a' | 'b'
const a: A = 'a';
const b: B = 'a';
console.log(a, b);
EOF
npx tsc --noEmit exclude.ts
# (no errors)
# ============================================
# PART 4: INFER
# ============================================
cat > infer.ts << 'EOF'
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;
type A = MyReturnType<() => string>;
// string
type B = MyParameters<(x: number, y: string) => void>;
// [x: number, y: string]
type C = MyAwaited<Promise<Promise<number>>>;
// number
const a: A = 'hello';
const b: B = [1, 'x'];
const c: C = 42;
console.log(a, b, c);
EOF
npx tsc --noEmit infer.ts
# (no errors)
# ============================================
# PART 5: KEYS BY VALUE TYPE
# ============================================
cat > keys.ts << 'EOF'
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
interface User {
id: number;
name: string;
email: string;
active: boolean;
}
type NumberKeys = KeysOfType<User, number>;
// 'id'
type StringKeys = KeysOfType<User, string>;
// 'name' | 'email'
const a: NumberKeys = 'id';
const b: StringKeys = 'name';
console.log(a, b);
EOF
npx tsc --noEmit keys.ts
# (no errors)
# ============================================
# PART 6: CHAINED CONDITIONALS
# ============================================
cat > chained.ts << 'EOF'
type Describe<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
T extends Function ? 'function' :
T extends object ? 'object' :
'unknown';
type A = Describe<string>;
type B = Describe<42>;
type C = Describe<() => void>;
type D = Describe<{ x: 1 }>;
const a: A = 'string';
const b: B = 'number';
const c: C = 'function';
const d: D = 'object';
console.log(a, b, c, d);
EOF
npx tsc --noEmit chained.ts
# (no errors)
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc basic.ts distribution.ts exclude.ts infer.ts keys.ts chained.ts
node basic.js
# [ yes no yes ]
node distribution.js
# [ [ 'hello' ] [ 42 ] [ 'hello', 42 ] ]
node exclude.js
# [ a a ]
node infer.js
# [ hello [ 1, 'x' ] 42 ]
node keys.js
# [ id name ]
node chained.js
# [ string number function object ]
Quick Reference
Syntax
| Form | Meaning |
|---|---|
T extends U ? X : Y | If T is assignable to U, X; else Y |
T extends U ? infer V : never | Capture a type |
[T] extends [U] ? X : Y | No distribution |
infer X extends Y | Constrained infer |
| Nested | T extends A ? X : T extends B ? Y : Z |
Distribution Rules
| Left side | Behavior |
|---|---|
| Naked type param | Distributes |
[T] | No distribution |
| Concrete type | No distribution |
never | Result is never |
infer Positions
| Pattern | Captures |
|---|---|
T extends (infer U)[] | Element of array |
T extends Promise<infer U> | Value of promise |
T extends (...args: infer P) => any | Parameters |
T extends (...args: any) => infer R | Return type |
T extends Record<string, infer V> | Value type |
T extends new (...args: any) => infer R | Instance type |
T extends [infer A, ...infer Rest] | First + rest |
Built-in Conditional Types
| Type | Definition |
|---|---|
Exclude<T, U> | T extends U ? never : T |
Extract<T, U> | T extends U ? T : never |
NonNullable<T> | T extends null | undefined ? never : T |
ReturnType<T> | T extends (...a) => infer R ? R : never |
Parameters<T> | T extends (...a: infer P) => any ? P : never |
Awaited<T> | Unwrap promise recursively |
InstanceType<T> | T extends new (...a) => infer R ? R : never |
Common Patterns
| Pattern | Purpose |
|---|---|
| Filter union | T extends U ? never : T |
| Keep matching | T extends U ? T : never |
| Extract return | T extends (...a) => infer R ? R : never |
| Extract params | T extends (...a: infer P) => any ? P : never |
| Unwrap promise | T extends Promise<infer U> ? U : T |
| Keys by value | {[K in keyof T]: T[K] extends V ? K : never}[keyof T] |
| Array element | T extends (infer U)[] ? U : T |
| Function test | T extends (...a: any) => any ? true : false |
Equality Testing
type Equals<A, B> =
(<T>() => T extends A ? 1 : 2) extends
(<T>() => T extends B ? 1 : 2) ? true : false;
When to Prevent Distribution
| Goal | Solution |
|---|---|
| Match whole union | [T] extends [U] |
| Exact equality | <T>() => T extends A ? 1 : 2 trick |
| Non-distributive filter | Wrap both sides in tuples |
Utility Combinations
| Combination | Result |
|---|---|
Exclude<T, U> | Remove U from T |
Extract<T, U> | Keep only U from T |
NonNullable<T> | Remove null/undefined |
Awaited<ReturnType<T>> | Async function’s value |
Parameters<typeof fn> | Function’s params |
InstanceType<typeof C> | Class instance |
Error Cases
| Error | Cause | Fix |
|---|---|---|
infer not allowed | Wrong position | Use in true branch |
| Infinite recursion | No base case | Add terminal case |
| Type too complex | Deep recursion | Simplify |
| Unintended distribution | Naked type param | Wrap in [ ] |
Recursion
| Type | Pattern |
|---|---|
Awaited<T> | T extends Promise<infer U> ? Awaited<U> : T |
DeepReadonly<T> | T extends object ? {...} : T |
Flatten<T> | T extends (infer U)[] ? Flatten<U> : T |
UnionToIntersection<T> | Function trick |
Common Utilities to Know
| Type | Returns |
|---|---|
T[K] | Property type |
keyof T | Keys |
Extract | Subset of union |
Exclude | Complement |
NonNullable | Without null |
ReturnType | Function return |
Parameters | Function args |
InstanceType | Class instance |
Awaited | Resolved promise |
Best Practices
โ Do This:
// Use infer to extract types
type ReturnType<T> = T extends (...a: any) => infer R ? R : never; // โ
// Use distribution for filtering unions
type MyExclude<T, U> = T extends U ? never : T; // โ
// Prevent distribution when matching whole unions
type IsUnion<T> = [T] extends [infer U]
? [T] extends [U] ? false : true
: never; // โ
// Combine conditional + mapped for keys by type
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T]; // โ
// Constrain infer when useful
type FirstString<T> =
T extends [infer S extends string, ...unknown[]] ? S : never; // โ
// Handle never explicitly
type NonNullish<T> = T extends null | undefined ? never : T; // โ
// Recursive conditionals need a base case
type DeepUnwrap<T> =
T extends Promise<infer U> ? DeepUnwrap<U> : T; // โ
โ Don’t Do This:
// Don't use infer outside the true branch
type Bad<T> = T extends string ? 'yes' : infer R; // โ // โ
// Don't forget about distribution
type NonString<T> = T extends string ? never : T;
// NonString<string | number> = number, not string | number // โ ๏ธ
// Don't chain too deeply without naming
type Complex<T> =
T extends A ? B : T extends C ? D : T extends E ? F : G; // โ ๏ธ // โ ๏ธ
// Don't recurse without a base case
type Loop<T> = T extends string ? Loop<T> : T; // โ ๏ธ infinite // โ ๏ธ
// Don't assume T is a union
type Bad<T> = T extends any ? T[] : never; // โ ๏ธ distributes // โ ๏ธ
// Don't use naked T when you mean the whole union
type IsArray<T> = T extends any[] ? true : false;
// IsArray<string[] | number[]> = true | true = true // โ ๏ธ
// Don't over-nest conditionals
// Extract to named types // โ ๏ธ
// Don't ignore never
type Bad<T> = T extends string ? T : T; // โ ๏ธ never in = never out // โ ๏ธ
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Unintended distribution | Wrong result for unions | Wrap in [ ] |
infer outside true branch | Compile error | Use in the pattern |
Forgetting never result | Lost matches | Check the empty case |
| Infinite recursion | Type complexity error | Add base case |
| Deep nesting | Unreadable | Extract named types |
any distribution | Distributes oddly | Handle explicitly |
| Function test | Not a value | Use Function type |
| Tuple inference | Wrong shape | Match tuple syntax |
Missing extends constraint | Too loose | Add constraint |
Real-World Examples
1. Extract array element
type ElementOf<T> = T extends (infer U)[] ? U : never;
2. Get promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;
3. Deep unwrap
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
4. Function return
type ReturnType<T> = T extends (...args: any) => infer R ? R : never;
5. Function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
6. Class instance
type Instance<T> = T extends new (...args: any) => infer R ? R : never;
7. Exclude union member
type Exclude<T, U> = T extends U ? never : T;
8. Extract union member
type Extract<T, U> = T extends U ? T : never;
9. Remove nullish
type NonNullish<T> = T extends null | undefined ? never : T;
10. Test if function
type IsFunction<T> = T extends (...args: any) => any ? true : false;
11. Test if array
type IsArray<T> = T extends unknown[] ? true : false;
12. Keys with value type
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
13. Values matching type
type ValuesOfType<T, V> = {
[K in keyof T]: T[K] extends V ? T[K] : never;
}[keyof T];
14. First tuple element
type First<T> = T extends [infer F, ...unknown[]] ? F : never;
15. Last tuple element
type Last<T> = T extends [...unknown[], infer L] ? L : never;
16. Tuple to union
type TupleToUnion<T extends readonly unknown[]> = T[number];
17. Union to intersection
type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends
(x: infer I) => void ? I : never;
18. Flatten array
type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T;
19. Deep partial
type DeepPartial<T> =
T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
20. Deep readonly
type DeepReadonly<T> =
T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
Visual: Conditional Type
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ T extends U ? X : Y โ
โ โโโฌโโโโโ โโฌโ โโโฌโโ โโฌ โ
โ โ โ โ โ โ
โ โ โ โ โ false branch โ
โ โ โ โ true branch โ
โ โ โ condition โ
โ โ input type โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ IsString<string> โ
โ โ
โ string extends string ? โ
โ โ yes โ 'yes' โ
โ โ
โ IsString<number> โ
โ โ
โ number extends string ? โ
โ โ no โ 'no' โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Distribution
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ToArray<T> = T extends unknown ? T[] : neverโ
โ โ
โ ToArray<string | number> โ
โ โ โ
โ โผ โ
โ distributes over union: โ
โ โ
โ ToArray<string> = string[] โ
โ ToArray<number> = number[] โ
โ โ โ
โ โผ โ
โ string[] | number[] โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ NonString<T> = T extends string ? never : T โ
โ โ
โ NonString<string | number> โ
โ โ โ
โ โผ โ
โ NonString<string> = never โ
โ NonString<number> = number โ
โ โ โ
โ โผ โ
โ never | number = number โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Preventing Distribution
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ With distribution: โ
โ โ
โ type IsString<T> = T extends string ? true : false;โ
โ โ
โ IsString<string | number> โ
โ โ string extends string โ true โ
โ โ number extends string โ false โ
โ โ true | false โ
โ โ boolean โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Without distribution: โ
โ โ
โ type IsString<T> = [T] extends [string] ? true : false;โ
โ โ
โ IsString<string | number> โ
โ โ (string | number) extends string? โ
โ โ no โ false โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: infer
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ type ReturnType<T> = โ
โ T extends (...args: any[]) => infer R โ
โ ? R โ
โ : never; โ
โ โ
โ ReturnType<() => string> โ
โ โ โ
โ โผ โ
โ () => string matches pattern โ
โ R is captured as string โ
โ โ โ
โ โผ โ
โ result: string โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ReturnType<number> โ
โ โ โ
โ โผ โ
โ number doesn't match function pattern โ
โ โ โ
โ โผ โ
โ result: never โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Chained Conditionals
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TypeName<T> = โ
โ T extends string ? 'string' : โ
โ T extends number ? 'number' : โ
โ T extends boolean ? 'boolean' : โ
โ T extends Function ? 'function' : โ
โ 'object'; โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TypeName<string> โ
โ โ โ
โ โผ โ
โ match first โ 'string' โ
โ โ
โ TypeName<number> โ
โ โ โ
โ โผ โ
โ no match first โ check second โ 'number' โ
โ โ
โ TypeName<() => void> โ
โ โ โ
โ โผ โ
โ match function โ 'function' โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: KeysOfType Pattern
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ type KeysOfType<T, V> = { โ
โ [K in keyof T]: T[K] extends V ? K : neverโ
โ }[keyof T]; โ
โ โ
โ interface User { โ
โ id: number; โ
โ name: string; โ
โ email: string; โ
โ active: boolean; โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ map
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ { โ
โ id: number extends number ? 'id' : never; โ
โ name: string extends number ? 'name' : never;โ
โ email: string extends number ? 'email' : never;โ
โ active: boolean extends number ? 'active' : never;โ
โ } โ
โ โ โ
โ โผ โ
โ { โ
โ id: 'id'; โ
โ name: never; โ
โ email: never; โ
โ active: never; โ
โ } โ
โ โ โ
โ โผ [keyof User] โ
โ 'id' | never | never | never = 'id' โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Recursion
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ type DeepUnwrap<T> = โ
โ T extends Promise<infer U> ? DeepUnwrap<U> : Tโ
โ โ
โ DeepUnwrap<Promise<Promise<string>>> โ
โ โ โ
โ โผ โ
โ Promise<Promise<string>> matches โ
โ U = Promise<string> โ
โ DeepUnwrap<Promise<string>> โ
โ โ โ
โ โผ โ
โ Promise<string> matches โ
โ U = string โ
โ DeepUnwrap<string> โ
โ โ โ
โ โผ โ
โ string doesn't match โ string โ
โ โ
โ Result: string โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: The never Filter
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Exclude<'a' | 'b' | 'c', 'b'> โ
โ โ
โ distributes: โ
โ โ
โ 'a' extends 'b' ? never : 'a' โ 'a' โ
โ 'b' extends 'b' ? never : 'b' โ never โ
โ 'c' extends 'b' ? never : 'c' โ 'c' โ
โ โ
โ combine: โ
โ โ
โ 'a' | never | 'c' โ
โ โ
โ never disappears in unions: โ
โ โ
โ 'a' | 'c' โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Decision Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Need to branch on a type? โ
โ โ โ
โ โโโ Use conditional type โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Need to extract part of a type? โ
โ โ โ
โ โโโ Use infer โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Working with a union? โ
โ โ โ
โ โโโ Filter per member โโโบ distribute โ
โ โ โ
โ โโโ Match whole union โโโบ [T] extends [U]โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Combining with mapped types? โ
โ โ โ
โ โโโ Keys by value, filters, etc. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Recursive? โ
โ โ โ
โ โโโ Always include a base case โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Concept | Meaning |
|---|---|
| Conditional type | T extends U ? X : Y |
| Distribution | Applies per union member |
| Non-distribution | [T] extends [U] |
infer | Capture a type in the pattern |
infer X extends Y | Constrained infer |
| Nested | Chained conditionals |
| Recursive | Reference itself |
never filter | Remove union members |
Key takeaways:
- A conditional type is
T extends U ? X : Yโ TypeScript’sifat the type level extendsin a conditional means assignable-to, not inheritance- Distribution applies the conditional to each member of a union, combining results
neverdisappears in union composition โ that’s how filtering works[T] extends [U]turns off distribution โ match the whole union as oneinfercaptures a type at a position in the pattern โinfer Rfor return types,infer Ufor promise valuesinfer X extends Yconstrains the captured type- Utility types โ
Exclude,Extract,NonNullable,ReturnType,Parameters,Awaited,InstanceTypeโ are all conditional types - Chaining conditionals handles multiple cases, like a type-level
switch - Recursion works โ include a base case that terminates
KeysOfTypecombines mapped types, conditional types, and indexed accessEqualsuses a function-type trick to test exact equality- Reach for conditional types when the result depends on the shape of the input type
Remember: Conditional types are TypeScript’s type-level branching. They read like if statements and work like pattern matching. Distribution over unions is what makes Exclude and Extract possible; infer is how you extract types from structures. Every utility type you’ve used โ ReturnType, Parameters, Awaited, NonNullable โ is a small conditional. Once you can write them, you can read any library’s type helpers and build your own. They’re the bridge between simple generics and the full power of TypeScript’s type system.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!