Union types

JavaScript functions often accept more than one kind of value: a meter ID that may arrive as a string or a number, a setting that is a number or null. A union type describes exactly that. Write the possible types separated by | (read it as "or"):

type MeterId = string | number;

function formatId(id: MeterId): string {
  return `meter ${id}`;
}

console.log(formatId("E-1001"));
console.log(formatId(1001));
meter E-1001
meter 1001

The name comes from the union of sets: if a type is the set of values it allows, string | number is the set of all strings together with all numbers. A union can have as many members as you need, and unions with null or undefined (number | null) are the standard way to say "may be missing" (see null, undefined and Strict Null Checks).

Working with a union value

Accepting a union is easy; using it takes one more step. The compiler only allows operations that are valid for every member. toUpperCase exists on strings but not on numbers, so it is refused:

function formatId(id: string | number): string {
  return id.toUpperCase();
}
example.ts(2,13): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.

Methods that both members share are fine, such as toString(). For everything else, check which member you have. Inside an if (typeof id === "number") block, TypeScript knows id is a number, and after it, that it must be a string. This is called narrowing, and it has its own page: Narrowing and Type Guards.

function formatId(id: string | number): string {
  if (typeof id === "number") {
    return `E-${id}`;
  }
  return id.toUpperCase();
}

console.log(formatId(1001));
console.log(formatId("e-1002"));
E-1001
E-1002

The same rule applies to unions of object types: you can read a property only when every member has it. With Outage | Reading, reading e.ticket fails because a Reading has no ticket (TS2339), while a property both types share is allowed. A union of objects that share a "kind" property is the most useful pattern in TypeScript; it is explained under discriminated unions.

Literal types

A literal type is a type with exactly one value: the type "day" allows only the string "day", the type 2 only the number 2. On its own that is not very useful. Combined in a union, it is one of TypeScript's most practical features, a type for "one of these values":

type Tariff = "day" | "night";

function pricePerKWh(tariff: Tariff): number {
  return tariff === "day" ? 0.32 : 0.25;
}

console.log(pricePerKWh("day"));
console.log(pricePerKWh("night"));
0.32
0.25

Every call site is now checked against the list, including capitalization. Editors also offer the allowed values as autocompletion:

type Tariff = "day" | "night";

function pricePerKWh(tariff: Tariff): number {
  return tariff === "day" ? 0.32 : 0.25;
}

console.log(pricePerKWh("Day"));
example.ts(7,25): error TS2345: Argument of type '"Day"' is not assignable to parameter of type 'Tariff'.

Number and boolean literals work the same way. A ticket priority from 1 to 4 and a fixed set of statuses become precise types, and a value outside them is caught:

type Priority = 1 | 2 | 3 | 4;
type Status = "open" | "in-progress" | "resolved";

interface Ticket {
  id: string;
  priority: Priority;
  status: Status;
}

const t: Ticket = { id: "INC-30412", priority: 2, status: "open" };
console.log(`${t.id} P${t.priority} ${t.status}`);

const urgent: Ticket = { id: "INC-30415", priority: 5, status: "open" };
example.ts(13,43): error TS2322: Type '5' is not assignable to type 'Priority'.

The compiler also uses literal types to spot comparisons that can never be true. If a status can only be one of three values, testing it against a fourth is almost certainly a bug, perhaps a value that was renamed:

type Status = "open" | "in-progress" | "resolved";

function isWaiting(s: Status): boolean {
  return s === "pending";
}
example.ts(4,10): error TS2367: This comparison appears to be unintentional because the types 'Status' and '"pending"' have no overlap.

Type widening

Which type does TypeScript infer for "day"? It depends on where the value is stored. A const can never change, so it keeps the literal type "day". A let variable may be reassigned later, so its type is widened to string. This type widening is usually what you want, but it explains a common surprise:

let tariff = "day";
const fixed = "day";

type Tariff = "day" | "night";
function pricePerKWh(t: Tariff): number {
  return t === "day" ? 0.32 : 0.25;
}

console.log(pricePerKWh(fixed));
console.log(pricePerKWh(tariff));
example.ts(10,25): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Tariff'.

The call with fixed (line 9) is accepted; the one with tariff (line 10) is not, because tariff is a string and could hold any text by then. Properties of an object literal widen too, even inside a const object, because properties can be reassigned:

type Tariff = "day" | "night";
function pricePerKWh(t: Tariff): number {
  return t === "day" ? 0.32 : 0.25;
}

const contract = { customer: "C-20417", tariff: "night" };
console.log(pricePerKWh(contract.tariff));
example.ts(7,25): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Tariff'.

There are two clean fixes. Annotate the object with a type that uses the literal union, so the compiler knows what tariff is meant to be. Or add as const, which tells the compiler to keep every value as its literal type and make the properties readonly (more on Enums, const Assertions and satisfies):

type Tariff = "day" | "night";
interface Contract {
  customer: string;
  tariff: Tariff;
}
function pricePerKWh(t: Tariff): number {
  return t === "day" ? 0.32 : 0.25;
}

const contract: Contract = { customer: "C-20417", tariff: "night" };
console.log(pricePerKWh(contract.tariff));

const other = { customer: "C-20418", tariff: "day" } as const;
console.log(pricePerKWh(other.tariff));
0.25
0.32

Contextual typing

Inference also works in the other direction. With contextual typing, an expression gets its type from the place where it is used. The classic case is a callback: readings.map(...) knows it is called on a number[], so the arrow function's first parameter is a number, without any annotation. The same happens when you assign a function to a variable whose function type is already declared:

const readings = [4.2, 3.9, 5.1];

const labels = readings.map((kWh, i) => `#${i + 1}: ${kWh.toFixed(1)} kWh`);
console.log(labels);

const handler: (ticketId: string, minutes: number) => string = (id, min) =>
  `${id} open for ${min} min`;
console.log(handler("INC-30412", 45));
[ '#1: 4.2 kWh', '#2: 3.9 kWh', '#3: 5.1 kWh' ]
INC-30412 open for 45 min

The inferred parameter types are fully checked. Treating a reading as a string inside the callback is caught, even though you never wrote number anywhere:

const readings = [4.2, 3.9, 5.1];
readings.forEach((kWh) => console.log(kWh.toUpperCase()));
example.ts(2,43): error TS2339: Property 'toUpperCase' does not exist on type 'number'.

Without a context, there is nothing to infer from. The same arrow function written on its own needs parameter annotations, or strict mode reports an implicit any (see Basic Types and Type Annotations):

const handler = (id, min) => `${id} open for ${min} min`;
example.ts(1,18): error TS7006: Parameter 'id' implicitly has an 'any' type.

Contextual typing also decides literal types: in const c: Contract = { tariff: "night" } above, the annotation is the context that keeps "night" a literal instead of widening it to string.

Intersection types

An intersection type, written A & B (read "and"), describes values that are of type A and of type B at the same time. For object types, that means the value must have all properties of both, so an intersection is a way to combine shapes:

interface Contact {
  name: string;
  email: string;
}
interface Address {
  street: string;
  city: string;
}
type Customer = Contact & Address & { id: string };

const c: Customer = {
  id: "C-20417",
  name: "Amara Okafor",
  email: "amara@example.com",
  street: "Oudegracht 12",
  city: "Utrecht",
};
console.log(`${c.name}, ${c.street}, ${c.city}`);
Amara Okafor, Oudegracht 12, Utrecht
interface Contact {
  name: string;
  email: string;
}
interface Address {
  street: string;
  city: string;
}
type Customer = Contact & Address;

const c: Customer = { name: "Joost Bakker", email: "joost@example.com", city: "Delft" };
example.ts(11,7): error TS2322: Type '{ name: string; email: string; city: string; }' is not assignable to type 'Customer'.

The naming can feel backward at first. An intersection has more properties than either part, and a union lets you use fewer. The set picture resolves it: a type is the set of values that fit it. Values that fit both Contact and Address are the overlap of the two sets, the intersection, and those are exactly the objects that carry both sets of properties.

Union and intersection as sets of values. Two overlapping circles: the left circle holds all values that fit Contact (they have name and email), the right circle all values that fit Address (they have street and city). The union Contact or Address covers both circles: a value from either side is accepted, but only what both sides share can be used without a check. The intersection Contact and Address is the overlap in the middle: values that have name, email, street and city. Contact name, email Address street, city Contact & Address all four Contact | Address: anything inside either circle Contact & Address: the overlap
A union accepts values from either circle; an intersection only those in the overlap, which have the properties of both types.

Intersections are how a type alias extends another type, the counterpart of interface … extends (see Interface or type?). When both parts declare the same property with incompatible types, there is no value that can satisfy both, and the property's type becomes never, the empty type:

type A = { priority: number };
type B = { priority: string };
type C = A & B;
const x: C = { priority: 1 };
example.ts(4,16): error TS2322: Type 'number' is not assignable to type 'never'.

interface … extends reports such a conflict at the declaration, which is one reason many teams prefer it for building object types from other object types. Intersections shine for ad-hoc combinations, such as "a ticket plus timestamps", and in generic helper functions.

Choosing between them

You want to say Use Example
The value is one of several types Union string | number
The value is one of a fixed set of values Union of literal types "open" | "in-progress" | "resolved"
The value may be missing Union with null or undefined number | null
The value is one of several shapes Discriminated union { kind: "outage"; … } | { kind: "reading"; … }
The value has all properties of several types Intersection (or interface extends) Contact & Address

Remember that these are all compile-time descriptions. The emitted JavaScript contains no trace of Tariff or Priority; a value that arrives from outside the program is not checked against them unless you write the check yourself.