TypeScript conditional types let your function return type depend on its input
← Back
April 4, 2026NodeJS6 min read

TypeScript conditional types let your function return type depend on its input

Published April 4, 20266 min read

I had a function that returned different shapes depending on a boolean flag. Without conditional types, I used function overloads — verbose and repetitive. Conditional types let me express "if you pass true, you get this type; if false, you get that type" in a single, clean signature. Here is the pattern.

The basic conditional type

typescript
// T extends U ? TypeIfTrue : TypeIfFalse
type IsString = T extends string ? true : false;

type A = IsString;  // true
type B = IsString;  // false
type C = IsString<'hello'>; // true (literal extends string)

Function with conditional return type

typescript
// Function that returns either one item or an array depending on input
type GetResult = Multiple extends true ? User[] : User;

async function getUser(
  id: string,
  multiple: T
): Promise> {
  if (multiple) {
    return db.users.findMany() as GetResult;
  }
  return db.users.findOne(id) as GetResult;
}

const one = await getUser('123', false);   // Type: User (not User[])
const many = await getUser('123', true);   // Type: User[] (not User)

The infer keyword: extract types

typescript
// Extract the element type from an array
type ElementOf = T extends (infer E)[] ? E : never;

type StrEl = ElementOf;   // string
type NumEl = ElementOf;   // number
type ObjEl = ElementOf<{id: string}[]>; // {id: string}

// Extract Promise's resolved type (like Awaited)
type Resolve = T extends Promise ? R : T;

type StrFromPromise = Resolve>;  // string
type NumPassthrough = Resolve;            // number (not a Promise)

// Extract function parameters and return type
type FirstParam = T extends (first: infer F, ...rest: unknown[]) => unknown ? F : never;

type Fn = (user: User, options: Options) => Promise;
type FirstArg = FirstParam;  // User

Real-world: serializer with conditional types

typescript
// Serialize primitive types to JSON-safe equivalents
type Serialize =
  T extends Date ? string :
  T extends bigint ? string :
  T extends undefined ? never :
  T extends Array ? Array> :
  T extends object ? { [K in keyof T]: Serialize } :
  T;

interface Order {
  id: string;
  createdAt: Date;
  amount: bigint;
  items: Array<{ productId: string; qty: number }>;
}

type SerializedOrder = Serialize;
// {
//   id: string;
//   createdAt: string;   // Date -> string
//   amount: string;      // bigint -> string
//   items: Array<{ productId: string; qty: number }>;
// }

Distributive conditional types

typescript
// Conditional types distribute over unions
type ToArray = T extends unknown ? T[] : never;

type Distributed = ToArray;
// string[] | number[]  (distributed)

// NOT distributed (wrapped in tuple):
type NotDistributed = [T] extends [unknown] ? T[] : never;
type Single = NotDistributed;
// (string | number)[]  (single array, not distributed)

// Useful: filter a union by type
type OnlyStrings = T extends string ? T : never;
type Mixed = 'a' | 'b' | 1 | 2 | true;
type JustStrings = OnlyStrings;  // 'a' | 'b'

// Remove null and undefined from a type (what NonNullable does)
type StripNullish = T extends null | undefined ? never : T;
type Clean = StripNullish;
// string | number

The mental model for conditional types: they are like ternary operators but at the type level. T extends U ? A : B asks "does T extend U?" and produces type A or B based on the answer. The infer keyword lets you name a type you want to extract from inside another type. These two tools together let you write type transformations that would otherwise require function overloads or type assertions — keeping your code both type-safe and DRY.

Share this
← All Posts6 min read