Skip to content

Unions, Literals, and Narrowing

Union types, literal types, and type narrowing are powerful tools in TypeScript. They help you write code that is more flexible, but still safe in terms of types. In this section, we will learn how to use union types for values that can be one of many types, how to make literal types for specific allowed values, and how to narrow down types using type guards and discriminated unions. By the end of this section, you will understand how to work with unions, literals, and narrowing in TypeScript.

let id: string | number;

A union means a value is allowed to be one of more than one type. Here, id can be either a string or a number.

type Status = "pending" | "success" | "failed";

Literal types let you set exact allowed values, instead of just a broad type like string. This means Status can only be one of these three exact words, nothing else.

Narrowing means you check what type a value actually is before you use it. This helps TypeScript (and you) know exactly what is safe to do with that value.

function printValue(value: string | number) {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else {
console.log(value.toFixed(2));
}
}

typeof

Used to check simple types like string, number, boolean.

instanceof

Used to check if something is an object made from a certain class.

in

Used to check if a property exists inside an object.

Custom guard

A reusable function you write yourself that returns value is Type.

type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; message: string };
function handleResponse<T>(response: ApiResponse<T>) {
if (response.status === "success") {
return response.data;
}
throw new Error(response.message);
}

A discriminated union is when each type in the union has a common field (like status here) that tells you which version of the type you are dealing with. This makes it easy for TypeScript to figure out which fields are available.

graph TD A[ApiResponse] --> B{status} B -->|success| C[data] B -->|error| D[message]
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius * shape.radius;
case "square":
return shape.side * shape.side;
default: {
const _never: never = shape;
return _never;
}
}
}

If someone adds a new shape type later but forgets to handle it in the switch statement, TypeScript will show an error at _never. This is a useful safety check, it makes sure you never miss handling a new case by mistake.