← 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