Union Types & Narrowing
Handling values that could be one of several types
Pixl presents
A value could be a string or a number, and TypeScript wants you to check first. Narrowing is how you stop guessing.A value could be a string or a number, and TypeScript wants you to check first. Narrowing is how you stop guessing.

Think about a parking spot. Could be a car in it, could be a truck, could be a motorcycle. Only ever one at a time though (unless somebody parked REALLY badly).
Union types work just like that. A variable with a union type can hold one of a few possible types, and TypeScript helps you figure out which one you've actually got.
What Are Union Types?#
A union type uses the pipe symbol (|) to say "this value can be type A or type B".
// This variable can hold a string OR a number
let id: string | number;
id = "abc-123"; // OK - it's a string
id = 42; // OK - it's a number
id = true; // Error! boolean is not string | numberYou'll run into union types all over the place in real apps.
// A function that accepts multiple types
function printId(id: string | number): void {
console.log(`ID is: ${id}`);
}
printId("user-123"); // OK
printId(456); // OK
// Common pattern: value or null
type MaybeUser = User | null;
function findUser(id: number): User | null {
// Returns User if found, null if not
}The Problem: What Can You Do With a Union?#
When a value could be more than one type, TypeScript only lets you do the stuff that works for EVERY type in the union.
function processValue(value: string | number) {
// These work - both string and number have toString()
console.log(value.toString());
// This would NOT work
console.log(value.toUpperCase()); // Error! number doesn't have toUpperCase
console.log(value.toFixed(2)); // Error! string doesn't have toFixed
}That's where narrowing comes in. You have to show TypeScript which specific type you're working with.
Type Narrowing with typeof#
The typeof operator checks the type while your code runs, and TypeScript is smart enough to understand it.
function processValue(value: string | number) {
if (typeof value === "string") {
// TypeScript knows value is a string here
console.log(value.toUpperCase()); // OK!
console.log(value.length); // OK!
} else {
// TypeScript knows value is a number here
console.log(value.toFixed(2)); // OK!
console.log(value * 2); // OK!
}
}TypeScript follows right along with your logic. Inside the if block, it narrows the type down based on the check you just did.
Narrowing with instanceof#
For class instances, reach for instanceof instead.
class Dog {
bark() { return "Woof!"; }
}
class Cat {
meow() { return "Meow!"; }
}
function makeSound(pet: Dog | Cat): string {
if (pet instanceof Dog) {
return pet.bark(); // TypeScript knows it's a Dog
} else {
return pet.meow(); // TypeScript knows it's a Cat
}
}Truthiness Narrowing#
Just checking whether a value exists is enough to narrow away null and undefined.
function printName(name: string | null | undefined) {
if (name) {
// TypeScript knows name is a string here (not null/undefined)
console.log(name.toUpperCase());
} else {
console.log("No name provided");
}
}Discriminated Unions#

This one's a big deal. Every member of the union shares a common property (called a "discriminant") that says which type it is.
interface Circle {
kind: "circle"; // Discriminant property
radius: number;
}
interface Rectangle {
kind: "rectangle"; // Discriminant property
width: number;
height: number;
}
interface Triangle {
kind: "triangle"; // Discriminant property
base: number;
height: number;
}
type Shape = Circle | Rectangle | Triangle;
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
// TypeScript knows shape is Circle
return Math.PI * shape.radius ** 2;
case "rectangle":
// TypeScript knows shape is Rectangle
return shape.width * shape.height;
case "triangle":
// TypeScript knows shape is Triangle
return (shape.base * shape.height) / 2;
}
}Real-World Example: API States#
Here's a super common pattern for juggling loading, success and error states.
interface LoadingState {
status: "loading";
}
interface SuccessState {
status: "success";
data: User[];
}
interface ErrorState {
status: "error";
message: string;
}
type FetchState = LoadingState | SuccessState | ErrorState;
function renderUI(state: FetchState): string {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
return `Found ${state.data.length} users`;
case "error":
return `Error: ${state.message}`;
}
}The in Operator for Narrowing#
You can also check whether an object has a certain property.
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish) {
if ("fly" in animal) {
animal.fly(); // TypeScript knows it's a Bird
} else {
animal.swim(); // TypeScript knows it's a Fish
}
}TL;DR#
- Union types (
A | B) let a value be one of a few types - TypeScript only lets you do things that work for every type in the union
- Narrowing is how you tell TypeScript which specific type you've got
- Use
typeoffor primitive checks andinstanceoffor class checks - Discriminated unions use a shared property to tell each variant apart
- The
inoperator checks whether a property exists, which narrows the type too
What's Next?#
You can handle values that might be different types now. Next we go deeper on arrays and objects... tuples, mapped types, and some handy utility types that come built right in.
This lesson ends with a short activity.