TypeScript Conditional Types

Create types based on conditions

❓ What are Conditional Types?

Conditional types select one of two possible types based on a condition. They work like ternary operators for types, using the syntax T extends U ? X : Y to create dynamic type logic.


type IsString<T> = T extends string ? "yes" : "no";
type Result1 = IsString<string>;  // "yes"
type Result2 = IsString<number>;  // "no"
                                    

Key Concepts

🔀

Type Branching

Choose types based on conditions

type Check<T> = T extends string 
  ? "text" : "other";
🎯

Type Inference

Extract types with infer keyword

type ReturnType<T> = T extends 
  (...args: any[]) => infer R ? R : never;
🔗

Distributive

Apply to union type members

type ToArray<T> = T extends any 
  ? T[] : never;
🧩

Nested Conditions

Chain multiple conditions

type TypeName<T> = T extends string 
  ? "string" : T extends number 
  ? "number" : "other";

🔹 Basic Conditional Type

Create types that branch based on conditions:

type IsNumber<T> = T extends number ? true : false;

type Test1 = IsNumber<42>;        // true
type Test2 = IsNumber<"hello">;   // false
type Test3 = IsNumber<boolean>;   // false

// Practical example
type MessageType<T> = T extends string ? "text" : "data";

type Msg1 = MessageType<string>;  // "text"
type Msg2 = MessageType<number>;  // "data"

🔹 Extracting Return Types

Use infer to extract types from functions:

type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getString(): string {
  return "hello";
}

function getNumber(): number {
  return 42;
}

type StringReturn = GetReturnType<typeof getString>;  // string
type NumberReturn = GetReturnType<typeof getNumber>;  // number

// Using built-in ReturnType utility
type Return1 = ReturnType<typeof getString>;  // string
type Return2 = ReturnType<typeof getNumber>;  // number

🔹 Extracting Array Element Types

Get the type of array elements:

type ArrayElement<T> = T extends (infer E)[] ? E : never;

type StringArray = string[];
type NumberArray = number[];

type Elem1 = ArrayElement<StringArray>;  // string
type Elem2 = ArrayElement<NumberArray>;  // number
type Elem3 = ArrayElement<string>;       // never (not an array)

// Practical usage
function getFirstElement<T extends any[]>(
  arr: T
): ArrayElement<T> | undefined {
  return arr[0];
}

const nums = [1, 2, 3];
const first = getFirstElement(nums);  // number | undefined

🔹 Distributive Conditional Types

Apply conditions to each member of a union:

type ToArray<T> = T extends any ? T[] : never;

type StringOrNumber = string | number;
type Result = ToArray<StringOrNumber>;
// Result: string[] | number[]

// Non-distributive version (wrap in tuple)
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<StringOrNumber>;
// Result2: (string | number)[]

// Practical example: Filter union types
type ExtractStrings<T> = T extends string ? T : never;
type Mixed = string | number | boolean;
type OnlyStrings = ExtractStrings<Mixed>;  // string

🔹 Nested Conditional Types

Chain multiple conditions for complex logic:

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 T1 = TypeName<"hello">;     // "string"
type T2 = TypeName<42>;          // "number"
type T3 = TypeName<true>;        // "boolean"
type T4 = TypeName<() => void>; // "function"
type T5 = TypeName<{}>;          // "object"

🔹 Practical Example: Flatten Type

Flatten nested arrays recursively:

type Flatten<T> = T extends any[] 
  ? T[number] extends infer U
    ? U extends any[]
      ? Flatten<U>
      : U
    : never
  : T;

type Nested = [1, [2, [3, [4]]]];
type Flat = Flatten<Nested>;  // 1 | 2 | 3 | 4

type SimpleArray = number[];
type FlatSimple = Flatten<SimpleArray>;  // number

type NotArray = string;
type FlatString = Flatten<NotArray>;  // string

🔹 Exclude and Extract Utilities

Filter union types with conditional types:

// Exclude: Remove types from union
type MyExclude<T, U> = T extends U ? never : T;

type AllTypes = string | number | boolean;
type NoStrings = MyExclude<AllTypes, string>;  // number | boolean

// Extract: Keep only matching types
type MyExtract<T, U> = T extends U ? T : never;

type OnlyNumbers = MyExtract<AllTypes, number>;  // number

// Built-in utilities
type Excluded = Exclude<AllTypes, string>;  // number | boolean
type Extracted = Extract<AllTypes, number>;  // number

🔹 Advanced: Function Parameter Types

Extract parameter types from functions:

type GetParameters<T> = T extends (...args: infer P) => any ? P : never;

function example(name: string, age: number, active: boolean) {
  return { name, age, active };
}

type Params = GetParameters<typeof example>;
// Result: [name: string, age: number, active: boolean]

// Using built-in Parameters utility
type Params2 = Parameters<typeof example>;
// Result: [name: string, age: number, active: boolean]

// Get first parameter
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any 
  ? F 
  : never;

type First = FirstParam<typeof example>;  // string

💡 Key Takeaways

  • Conditional types use T extends U ? X : Y syntax
  • Use infer keyword to extract types within conditions
  • Distributive behavior applies conditions to union members
  • Chain conditions for complex type logic
  • Built-in utilities like ReturnType , Parameters , Exclude use conditional types

🧠 Test Your Knowledge

What does T extends string ? "yes" : "no" do?