Expert · TypeScript basics · topic 53 of 64 · 35 min
Unions and narrowing
Explanation
Union types: one of several
function showId(id: string | number): string {
if (typeof id === "number") {
return "#" + id.toFixed(0); // here id is a number
}
return id.toUpperCase(); // here id is a string
}Literal types: a fixed list of words
type PaymentStatus = "pending" | "paid" | "failed";
let status: PaymentStatus = "pending";
status = "paid"; // OK
status = "refunded"; // Error: Type '"refunded"' is not assignable to type 'PaymentStatus'.null and undefined
With strictNullChecks (part of "strict": true), null and undefined are not allowed unless the type says so, for example User | undefined. TypeScript then makes you check before you use the value, which removes the classic "Cannot read properties of undefined" crash.
type User = { name: string; city?: string };
declare function findUser(id: string): User | undefined;
const user = findUser("u1");
user.name; // Error: 'user' is possibly 'undefined'.
if (user) {
user.name; // OK: narrowed to User
}
const city = user?.city ?? "Not given"; // optional chaining + defaultNarrowing: telling TypeScript which one it is
| Check | Example | Narrows to |
|---|---|---|
| typeof | typeof x === "string" | string, number, boolean and so on |
| === a literal | status === "paid" | that exact value |
| in | "upiId" in payment | the member that has that property |
| Array.isArray | Array.isArray(x) | the array member of the union |
| truthiness | if (user) | removes null and undefined |
| instanceof | err instanceof Error | that class |
Discriminated unions
Give every member of a union one shared field with a literal value, usually called kind, type or status. A switch on that field narrows to the exact member, so each case can use its own properties.
type Payment =
| { kind: "upi"; upiId: string }
| { kind: "card"; last4: string }
| { kind: "cash" };
function describe(p: Payment): string {
switch (p.kind) {
case "upi":
return "UPI: " + p.upiId; // p has upiId here
case "card":
return "Card ending " + p.last4; // p has last4 here
case "cash":
return "Cash on delivery";
default: {
const unreachable: never = p; // error if a case is missing
return unreachable;
}
}
}never is the type of a value that cannot exist. After all three cases, nothing is left, so p is never. If a teammate later adds { kind: "wallet" } and forgets a case, the never line turns red and points to the missing case. This is called an exhaustive check.
Your own type guards
type Product = { id: string; name: string; price: number };
function isProduct(value: unknown): value is Product {
return (
typeof value === "object" && value !== null &&
"id" in value && typeof value.id === "string" &&
"name" in value && typeof value.name === "string" &&
"price" in value && typeof value.price === "number"
);
}
if (isProduct(data)) {
console.log(data.price.toFixed(2)); // data is a Product here
}Watch the lesson
Unions and narrowing · 1/7
A union type means one of several types. The bar symbol reads as or.
Prefer a video by a teacher? Find free videos on “typescript union types narrowing discriminated unions” on YouTube ↗
In real life: Showing a UPI payment result
A bill-payment screen gets a result from the server: { status: "paid", txnId: "T123" } or { status: "failed", reason: "Wrong PIN" } or { status: "pending" }. The app models it as a discriminated union and uses a switch on status to show a green tick, a red message with the reason, or a spinner. When the team adds a new "refunded" status, the exhaustive check shows the one screen that still needs a message.
Practice (0/3 done)
type PaymentStatus = "pending" | "paid" | "failed". Write statusMessage(status: PaymentStatus): string using a switch. Return "Waiting for UPI approval" for "pending", "Payment received" for "paid", and "Payment failed, please try again" for "failed".
Text from an API or a URL is just a string. Write parsePaymentStatus(value: string): PaymentStatus | null. Trim the spaces and make it lowercase. If the result is "pending", "paid" or "failed", return it; otherwise return null. parsePaymentStatus(" PAID ") returns "paid" and parsePaymentStatus("refunded") returns null.
type Payment = { kind: "upi"; upiId: string } | { kind: "card"; last4: string } | { kind: "cash" }. Write describePayment(p: Payment): string that returns "UPI: <upiId>", "Card ending <last4>" or "Cash on delivery". describePayment({ kind: "card", last4: "4242" }) returns "Card ending 4242".
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.