Skip to content

Expert · TypeScript basics · topic 53 of 64 · 35 min

Unions and narrowing

Explanation

A union type means "one of these": string | number, or a fixed list of words like "pending" | "paid" | "failed". Before you use such a value, you check which one it is (with typeof, ===, in or a switch), and TypeScript narrows the type inside that branch. This is how you handle real data safely.

Union types: one of several

typescript
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
}
A | B means the value can be A or B; typeof tells them apart

Literal types: a fixed list of words

Think of it like this: A railway ticket status can only be CNF, RAC or WL. Nobody prints "maybe" or "almost" on a ticket. A union of literal types is that fixed list: the compiler accepts only the words you allowed.
typescript
type PaymentStatus = "pending" | "paid" | "failed";

let status: PaymentStatus = "pending";
status = "paid";     // OK
status = "refunded"; // Error: Type '"refunded"' is not assignable to type 'PaymentStatus'.
A UPI payment can only be in one of three states

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.

typescript
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 + default
Check first, or use ?. and ??

Narrowing: telling TypeScript which one it is

CheckExampleNarrows to
typeoftypeof x === "string"string, number, boolean and so on
=== a literalstatus === "paid"that exact value
in"upiId" in paymentthe member that has that property
Array.isArrayArray.isArray(x)the array member of the union
truthinessif (user)removes null and undefined
instanceoferr instanceof Errorthat 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.

typescript
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;
    }
  }
}
The kind field tells the members apart

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

typescript
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
}
value is Product: "when I return true, treat value as a Product"
Tip: Prefer a discriminated union over one type with many optional fields. { status: "paid"; paidAt: string } | { status: "failed"; reason: string } is clearer than one object with paidAt? and reason?, because each state carries exactly the fields it needs.
Careful: Truthiness checks also remove 0 and the empty string. if (amount) skips an amount of 0 rupees. When 0 is a valid value, check amount !== undefined instead.

Watch the lesson

Unions and narrowing · 1/7

string | number

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)

Task 1 · Code · Typescript

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".

Task 2 · Code · Typescript

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.

Task 3 · Code · Typescript

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

Question 1

1.let status: "pending" | "paid" | "failed" = "pending"; Which assignment is an error?

Question 2

2.function show(id: string | number) { ... } Which check lets you call id.toUpperCase() safely inside it?

Question 3

3.What is a discriminated union?

Question 4

4.Why write const unreachable: never = p; in the default branch of a switch over a union?

Question 5

5.function isProduct(value: unknown): value is Product. What does value is Product mean?

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.

← Previous: Typing functions and objects