TypeScript Tutorial menu

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

typescript
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

typescript
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));
Output
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

typescript
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'
Output
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

typescript
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");
Output
Not found

TypeScript will not let you call phone.length directly, because phone might be undefined.

Ways to Narrow

CheckExampleNarrows to
typeoftypeof x === "number"A primitive type
Truthinessif (x)Removes null, undefined and empty values
Equalityx === "cash"That literal value
in"price" in itemObject types that have that property
instanceofx instanceof DateThat class
Array.isArrayArray.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

typescript
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" }));
Output
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?

Learn TypeScript with a teacher

Join the Full Stack Development course at Workownix, online or in Orangi Town, Karachi, and get a certificate.

See the course Ask on WhatsApp