TypeScript Union Types and Narrowing
A union type lets a value be one of several types. Narrowing is how you find out which one it is.
Union Types
Join types with a vertical bar |, which you can read as "or":
Example
let rollNo: number | string;
rollNo = 117; // fine
rollNo = "WX-117"; // also fine
// rollNo = true; // Error: Type 'boolean' is not assignable to type 'string | number'This is useful when a value can really come in more than one form, like an ID that is sometimes a number and sometimes text.
Narrowing With typeof
With a union, TypeScript only lets you use what all the types have in common. To use type-specific methods, check the type first. Inside the if, TypeScript narrows the type.
Example
function formatId(id: number | string): string {
if (typeof id === "string") {
return id.toUpperCase(); // id is a string here
}
return "ID-" + id.toFixed(0); // id is a number here
}
console.log(formatId("wx-117"));
console.log(formatId(42));WX-117 ID-42
Literal Types
A literal type allows only one exact value. Combined into a union, literal types make a short list of allowed values:
Example
type Size = "S" | "M" | "L" | "XL";
type PaymentMethod = "cash" | "card" | "jazzcash" | "easypaisa";
function orderShirt(size: Size, pay: PaymentMethod): string {
return `Shirt size ${size}, paid by ${pay}`;
}
console.log(orderShirt("M", "jazzcash"));
// orderShirt("XXL", "cash"); // Error: Argument of type '"XXL"' is not assignable to parameter of type 'Size'Shirt size M, paid by jazzcash
Your editor also suggests the allowed values as you type, which saves time and spelling mistakes.
null and undefined in Unions
With strict mode on, a value that might be missing must say so with a union:
Example
function findPhone(name: string): string | undefined {
const book: Record<string, string> = { Ali: "0300-1112233" };
return book[name];
}
const phone = findPhone("Sara");
console.log(phone?.length ?? "Not found");Not found
TypeScript will not let you call phone.length directly, because phone might be undefined.
Ways to Narrow
| Check | Example | Narrows to |
|---|---|---|
typeof | typeof x === "number" | A primitive type |
| Truthiness | if (x) | Removes null, undefined and empty values |
| Equality | x === "cash" | That literal value |
in | "price" in item | Object types that have that property |
instanceof | x instanceof Date | That class |
Array.isArray | Array.isArray(x) | An array type |
Discriminated Unions
A very common pattern: each object type in the union has a shared property with a different literal value, often called kind or type. Checking that property narrows the whole object.
Example
type Cash = { kind: "cash"; amount: number };
type Card = { kind: "card"; amount: number; last4: string };
type Payment = Cash | Card;
function describe(p: Payment): string {
switch (p.kind) {
case "cash":
return `Rs ${p.amount} paid in cash`;
case "card":
return `Rs ${p.amount} paid by card ending ${p.last4}`;
}
}
console.log(describe({ kind: "cash", amount: 1500 }));
console.log(describe({ kind: "card", amount: 4200, last4: "9012" }));Rs 1500 paid in cash Rs 4200 paid by card ending 9012
Inside case "card", TypeScript knows p is a Card, so p.last4 is allowed.
If you add a new payment type later, TypeScript points out every switch that does not handle it yet, as long as the function has a return type. This makes big changes much safer.
Exercise
Exercise
Given type Light = "red" | "yellow" | "green"; which assignment is an error?
