Union Types & Narrowing
A union type means a value can be one of several types. Narrowing is how you tell TypeScript which specific type you're dealing with:
function formatValue(value: string | number): string {
// TypeScript doesn't know the type yet — you can't call .toFixed() or .toUpperCase()
if (typeof value === "string") {
// Narrowed to string
return value.toUpperCase();
}
// Narrowed to number (the only remaining option)
return value.toFixed(2);
}
formatValue("hello"); // "HELLO"
formatValue(3.14); // "3.14"
Discriminated Unions
A discriminated union uses a shared literal property (the discriminant) to distinguish between variants. This is one of the most powerful patterns in TypeScript:
interface LoadingState {
status: "loading";
}
interface SuccessState {
status: "success";
data: string[];
}
interface ErrorState {
status: "error";
message: string;
}
type RequestState = LoadingState | SuccessState | ErrorState;
function renderState(state: RequestState): string {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
// TypeScript knows state.data exists here
return `Got ${state.data.length} items`;
case "error":
// TypeScript knows state.message exists here
return `Error: ${state.message}`;
}
}This pattern replaces a lot of if/else chains and keeps your code type-safe at every branch.
Custom Type Guards
A type guard is a function that tells TypeScript what type a value is:
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
// Type predicate: `pet is Fish` narrows the type in the calling scope
function isFish(pet: Fish | Bird): pet is Fish {
return "swim" in pet;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // TypeScript knows this is Fish
} else {
pet.fly(); // TypeScript knows this is Bird
}
}The in Operator for Narrowing
interface Admin {
role: "admin";
permissions: string[];
}
interface Guest {
role: "guest";
expiresAt: Date;
}
type AppUser = Admin | Guest;
function describe(user: AppUser): string {
if ("permissions" in user) {
// Narrowed to Admin
return `Admin with ${user.permissions.length} permissions`;
}
// Narrowed to Guest
return `Guest access until ${user.expiresAt.toLocaleDateString()}`;
}Exhaustive Checks with never
The never type ensures you handle every variant of a union. If you add a new variant later, TypeScript will tell you about every switch you forgot to update:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
// If you miss a case, this line will have a compile error
const _exhaustive: never = shape;
return _exhaustive;
}
}Conditional Types
Conditional types choose a type based on a condition, similar to a ternary expression:
// Basic syntax: T extends U ? X : Y
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
// Practical example: extract the return type of a function
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type FnReturn = ReturnOf<()=> string>; // string
type FnReturn2 = ReturnOf<(x: number)=> boolean>; // boolean
Template Literal Types
Build string types from other types:
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// Practical: CSS property helper
type CSSUnit = "px" | "rem" | "em" | "%";
type CSSValue = `${number}${CSSUnit}`;
const width: CSSValue = "100px"; // OK
const height: CSSValue = "2.5rem"; // OK
// const bad: CSSValue = "100"; // Error
Mapped Types
Create new types by transforming properties of existing types:
// Make every property nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
interface User {
name: string;
age: number;
}
type NullableUser = Nullable<User>;
// { name: string | null; age: number | null }
// Make every property a getter function
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }