| |

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 condition
  • X โ€” the result when true
  • Y โ€” 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, and NonNullable work โ€” 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 infer in the true branch only: The pattern being matched is on the left of extends. infer introduces 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’s T extends (...args) => infer R ? R : never tells 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 Equals is subtle: TypeScript’s assignability is structural โ€” { a: 1 } and { a: 1 } are “the same.” But sometimes you want exact equality, including distinctions like any vs unknown. Equals uses 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 extends was added late: Before 4.7, you’d have to capture with infer and 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 via infer
  • Unwrap and DeepUnwrap โ€” unwrap promises, recursively
  • IsFunction โ€” tests if a type is callable
  • ReturnOrSelf โ€” returns the function’s return type or the type itself
  • KeysOfType โ€” conditional + mapped + indexed access
  • SafeProp โ€” safe property access with never for missing keys
  • Describe โ€” 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, never filtering, nesting โ€” combine into everything from ReturnType to KeysOfType. 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

FormMeaning
T extends U ? X : YIf T is assignable to U, X; else Y
T extends U ? infer V : neverCapture a type
[T] extends [U] ? X : YNo distribution
infer X extends YConstrained infer
NestedT extends A ? X : T extends B ? Y : Z

Distribution Rules

Left sideBehavior
Naked type paramDistributes
[T]No distribution
Concrete typeNo distribution
neverResult is never

infer Positions

PatternCaptures
T extends (infer U)[]Element of array
T extends Promise<infer U>Value of promise
T extends (...args: infer P) => anyParameters
T extends (...args: any) => infer RReturn type
T extends Record<string, infer V>Value type
T extends new (...args: any) => infer RInstance type
T extends [infer A, ...infer Rest]First + rest

Built-in Conditional Types

TypeDefinition
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

PatternPurpose
Filter unionT extends U ? never : T
Keep matchingT extends U ? T : never
Extract returnT extends (...a) => infer R ? R : never
Extract paramsT extends (...a: infer P) => any ? P : never
Unwrap promiseT extends Promise<infer U> ? U : T
Keys by value{[K in keyof T]: T[K] extends V ? K : never}[keyof T]
Array elementT extends (infer U)[] ? U : T
Function testT 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

GoalSolution
Match whole union[T] extends [U]
Exact equality<T>() => T extends A ? 1 : 2 trick
Non-distributive filterWrap both sides in tuples

Utility Combinations

CombinationResult
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

ErrorCauseFix
infer not allowedWrong positionUse in true branch
Infinite recursionNo base caseAdd terminal case
Type too complexDeep recursionSimplify
Unintended distributionNaked type paramWrap in [ ]

Recursion

TypePattern
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

TypeReturns
T[K]Property type
keyof TKeys
ExtractSubset of union
ExcludeComplement
NonNullableWithout null
ReturnTypeFunction return
ParametersFunction args
InstanceTypeClass instance
AwaitedResolved 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

PitfallProblemSolution
Unintended distributionWrong result for unionsWrap in [ ]
infer outside true branchCompile errorUse in the pattern
Forgetting never resultLost matchesCheck the empty case
Infinite recursionType complexity errorAdd base case
Deep nestingUnreadableExtract named types
any distributionDistributes oddlyHandle explicitly
Function testNot a valueUse Function type
Tuple inferenceWrong shapeMatch tuple syntax
Missing extends constraintToo looseAdd 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

ConceptMeaning
Conditional typeT extends U ? X : Y
DistributionApplies per union member
Non-distribution[T] extends [U]
inferCapture a type in the pattern
infer X extends YConstrained infer
NestedChained conditionals
RecursiveReference itself
never filterRemove union members

Key takeaways:

  • A conditional type is T extends U ? X : Y โ€” TypeScript’s if at the type level
  • extends in a conditional means assignable-to, not inheritance
  • Distribution applies the conditional to each member of a union, combining results
  • never disappears in union composition โ€” that’s how filtering works
  • [T] extends [U] turns off distribution โ€” match the whole union as one
  • infer captures a type at a position in the pattern โ€” infer R for return types, infer U for promise values
  • infer X extends Y constrains 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
  • KeysOfType combines mapped types, conditional types, and indexed access
  • Equals uses 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!