Conditional Type Basics

If-Else for Types

A conditional type A extends B ? X : Y means: "if type A is assignable to type B, the result is X, otherwise Y". It works like a ternary operator, but on types, at compile time.

Example

Types do not exist at runtime, so we "print" a type by assigning wrong values to it: the compiler error shows what the type resolved to. IsString<number> is false, so assigning true fails.

type IsString<T> = T extends string ? true : false;

const a: IsString<"hello"> = true;   // OK: "hello" extends string
const b: IsString<number> = true;    // error: type is false

Compiler output (tsc)

Line 4: error TS2322: Type 'true' is not assignable to type 'false'.

Conditional Return Types

Example

A function can return a different type depending on its argument type. Here the input decides whether you get one product or an array.

type Product = { id: number; name: string };
type LookupResult<T> = T extends number[] ? Product[] : Product;

const db: Product[] = [{ id: 1, name: "Kettle" }, { id: 2, name: "Toaster" }, { id: 3, name: "Iron" }];

function lookup<T extends number | number[]>(ids: T): LookupResult<T> {
  const find = (id: number): Product => db.find((p) => p.id === id) ?? { id, name: "?" };
  return (Array.isArray(ids) ? ids.map(find) : find(ids as number)) as LookupResult<T>;
}

const one = lookup(2);        // Product
const many = lookup([1, 3]);  // Product[]
console.log(one.name);
console.log(many.map((p) => p.name));

Output

Toaster
[ 'Kettle', 'Iron' ]

Note: Inside the implementation TypeScript cannot follow conditional types, so a cast (as LookupResult<T>) is normal here. Overloads are an alternative.

Testing Types With Type Tests

Example

Library authors write type tests: an Equal helper plus an Expect that only accepts true. If any line is wrong the file does not compile.

type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type IsString<T> = T extends string ? true : false;
type ElementType<T> = T extends readonly (infer E)[] ? E : T;

type tests = [
  Expect<Equal<IsString<"a">, true>>,
  Expect<Equal<IsString<42>, false>>,
  Expect<Equal<ElementType<string[]>, string>>,
  Expect<Equal<ElementType<number>, number>>,
];

console.log("All type tests compiled");

Output

All type tests compiled