Skip to main content
Practice Problems

Conditional types in TypeScript

What are Conditional Types?

Conditional Types are a TypeScript construct that allows choosing a type based on a condition. They work like ternary operators, but for types.

Syntax

typescript
T extends U ? X : Y

If type T can be assigned to type U, the result is type X, otherwise type Y.


Simple Example

typescript
type IsString<T> = T extends string ? true : false; type A = IsString<string>; // true type B = IsString<number>; // false type C = IsString<'hello'>; // true

How does it work?

  1. Checks if T is a subtype of string
  2. If yes — returns true
  3. If no — returns false

Practical Examples

Extracting Return Type

typescript
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; function getUserName() { return 'John'; } function getUserAge() { return 25; } type NameType = ReturnType<typeof getUserName>; // string type AgeType = ReturnType<typeof getUserAge>; // number

Filtering Types

typescript
type NonNullable<T> = T extends null | undefined ? never : T; type A = NonNullable<string | null>; // string type B = NonNullable<number | undefined>; // number type C = NonNullable<boolean | null | undefined>; // boolean

Checking for Array

typescript
type IsArray<T> = T extends any[] ? true : false; type A = IsArray<number[]>; // true type B = IsArray<string>; // false type C = IsArray<[1, 2, 3]>; // true (tuple is also array)

Distributive Conditional Types

When a conditional type is applied to a union type, it distributes over each member of the union.

typescript
type ToArray<T> = T extends any ? T[] : never; type A = ToArray<string | number>; // string extends any ? string[] : never | number extends any ? number[] : never // string[] | number[]

How does it work?

typescript
ToArray<string | number> // Distributes as: = ToArray<string> | ToArray<number> = string[] | number[]

Disabling Distribution

Use square brackets to prevent distribution:

typescript
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never; type A = ToArrayNonDist<string | number>; // (string | number)[]

The infer Keyword

infer allows "extracting" a type from a structure during conditional checking.

Unwrapping Promise Type

typescript
type Unwrap<T> = T extends Promise<infer U> ? U : T; type A = Unwrap<Promise<string>>; // string type B = Unwrap<Promise<number>>; // number type C = Unwrap<boolean>; // boolean

Extracting Function Argument Types

typescript
type FirstArg<T> = T extends (first: infer F, ...args: any[]) => any ? F : never; function test(name: string, age: number) { return { name, age }; } type First = FirstArg<typeof test>; // string

Extracting Array Element Type

typescript
type ElementType<T> = T extends (infer E)[] ? E : T; type A = ElementType<string[]>; // string type B = ElementType<number[]>; // number type C = ElementType<boolean>; // boolean

Nested Conditional Types

typescript
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<42>; // "number" type C = TypeName<() => void>; // "function" type D = TypeName<{}>; // "object"

Built-in Utilities Based on Conditional Types

TypeScript provides many built-in utilities implemented through conditional types.

Exclude

Excludes types from union:

typescript
type Exclude<T, U> = T extends U ? never : T; type A = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c' type B = Exclude<string | number, string>; // number

Extract

Extracts types from union:

typescript
type Extract<T, U> = T extends U ? T : never; type A = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a' type B = Extract<string | number, number>; // number

NonNullable

Removes null and undefined:

typescript
type NonNullable<T> = T extends null | undefined ? never : T; type A = NonNullable<string | null>; // string type B = NonNullable<number | undefined | null>; // number

ReturnType

Extracts function return type:

typescript
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any; function getUserData() { return { name: 'John', age: 25 }; } type UserData = ReturnType<typeof getUserData>; // { name: string; age: number; }

Advanced Patterns

Recursive Conditional Types

typescript
type DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]; }; interface User { name: string; address: { city: string; country: string; }; } type ReadonlyUser = DeepReadonly<User>; /* { readonly name: string; readonly address: { readonly city: string; readonly country: string; }; } */

Extracting Keys of Specific Type

typescript
type KeysOfType<T, U> = { [K in keyof T]: T[K] extends U ? K : never; }[keyof T]; interface User { id: number; name: string; age: number; email: string; } type StringKeys = KeysOfType<User, string>; // "name" | "email" type NumberKeys = KeysOfType<User, number>; // "id" | "age"

Making Required Fields Conditionally

typescript
type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>; interface User { name?: string; age?: number; email?: string; } type UserWithName = RequireKeys<User, 'name'>; // { name: string; age?: number; email?: string; }

Practical Use Cases

API Response Helper

typescript
type ApiResponse<T> = T extends { data: infer D } ? D : T; interface SuccessResponse { data: { id: number; name: string }; status: 'success'; } type Data = ApiResponse<SuccessResponse>; // { id: number; name: string; }

Flatten Union Types

typescript
type Flatten<T> = T extends Array<infer U> ? U : T; type A = Flatten<string[]>; // string type B = Flatten<number[][]>; // number[] type C = Flatten<(string | number)[]>; // string | number

Promise Chain Type

typescript
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T; type A = UnwrapPromise<Promise<string>>; // string type B = UnwrapPromise<Promise<Promise<number>>>; // number type C = UnwrapPromise<Promise<Promise<Promise<boolean>>>>; // boolean

Function or Value

typescript
type ValueOrFunction<T> = T | (() => T); type Resolve<T> = T extends (...args: any[]) => infer R ? R : T; type A = Resolve<string>; // string type B = Resolve<() => number>; // number

Limitations and Features

Recursion Depth

TypeScript limits recursive type depth to prevent infinite loops:

typescript
// May lead to "Type instantiation is excessively deep" error type DeepArray<T, N extends number = 10> = N extends 0 ? T : DeepArray<T[], Decrement<N>>;

Order of Checks Matters

typescript
// Order is important type TypeName<T> = T extends any[] ? "array" : // Check array first T extends object ? "object" : // Then object T extends string ? "string" : "other"; type A = TypeName<string[]>; // "array" (not "object")

Never in Conditional Types

typescript
type A = never extends string ? true : false; // true // never is a subtype of any type

Comparison with Other Approaches

Before Conditional Types

typescript
// Had to use overloads function wrap(x: string): string[]; function wrap(x: number): number[]; function wrap(x: any): any[] { return [x]; }

With Conditional Types

typescript
type Wrap<T> = T extends any ? T[] : never; function wrap<T>(x: T): Wrap<T> { return [x] as Wrap<T>; } const a = wrap('hello'); // string[] const b = wrap(42); // number[]

Common Mistakes

Forgetting about distributivity

typescript
type WrapInArray<T> = T extends any ? T[] : never; type A = WrapInArray<string | number>; // string[] | number[] // Expected (string | number)[], but got union of arrays // Correct: type WrapInArray<T> = [T] extends [any] ? T[] : never; type B = WrapInArray<string | number>; // (string | number)[]

Incorrect use of infer

typescript
// Wrong type Wrong<T> = T extends infer U ? U : never; // Meaningless // Correct type Correct<T> = T extends Promise<infer U> ? U : T;

Complex Logic in One Type

typescript
// Bad - hard to read type Complex<T> = T extends string ? T extends `${infer F}${infer R}` ? F extends 'a' ? true : false : false : false; // Good - split into parts type StartsWithA<T> = T extends `a${string}` ? true : false; type IsString<T> = T extends string ? true : false; type Complex<T> = IsString<T> extends true ? StartsWithA<T> : false;

Conclusion

Conditional Types:

  • Allow creating flexible and reusable types
  • Foundation for many built-in TypeScript utilities
  • Distribute over union types (distributive)
  • Support type extraction via infer
  • Can be recursive (with limitations)
  • Critically important for advanced typing

In Interviews:

Important to be able to:

  • Explain T extends U ? X : Y syntax
  • Show difference between distributive and non-distributive types
  • Use infer for type extraction
  • Provide examples of built-in utilities
  • Implement custom utilities based on conditional types

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?
Practice Problems