Hack Frontend Community

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

T extends U ? X : Y

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


Simple Example

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

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

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

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.

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?

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

Disabling Distribution

Use square brackets to prevent distribution:

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

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

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

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

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:

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:

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:

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:

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

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

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

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

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

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

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

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:

// 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

// 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

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

Comparison with Other Approaches

Before Conditional Types

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

With Conditional Types

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

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

// 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

// 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