Union Types in TypeScript

A union type lets a value be one of several types. Learn how to define unions, work with them safely, and avoid the most common union mistakes.

6 min read

TypeScript union types describe a value that can be one of several types. You write a union with a vertical bar between each type: string | number means "string or number." A variable with this type can hold a string or a number at any point, and the compiler only allows operations that are safe for every member.

typescripttypescript
let id: string | number;
 
id = "abc-123";   // OK
id = 42;           // OK
id = true;         // Error

Assigning a boolean fails because true is not part of the union. The compiler reports the mismatch before the code ever runs:

texttext
Type 'boolean' is not assignable to type 'string | number'.

You get this safety without writing a single runtime check, because the boundary is enforced entirely at compile time.

Why Union Types Exist

JavaScript functions often accept more than one kind of input, such as a string or a number, an element or null, an array or a single value. Without unions, typing these cases in plain JavaScript usually means falling back to any, which turns off type checking completely.

A union describes exactly which types are allowed, so the compiler can enforce the boundary while still letting the function accept real-world varied input.

typescripttypescript
function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + value;
  }
  return padding + value;
}
 
console.log(padLeft("Hello", 4));     // "    Hello"
console.log(padLeft("Hello", ">> ")); // ">> Hello"

The padding parameter accepts a string or a number, and the function does not know which one it holds until it checks. The typeof check narrows the type before calling the number-only repeat method, so the compiler allows the call.

Defining a Union Type

Write each member type separated by a pipe character. You can combine primitives, object types, literal types, and arrays this way.

typescripttypescript
type Status = "idle" | "loading" | "success" | "error";
 
function setStatus(s: Status) {
  console.log(`Status set to: ${s}`);
}
 
setStatus("loading"); // OK
setStatus("paused");  // Error

A union of string literals creates a fixed set of allowed values, one of the most common union patterns in everyday TypeScript code. Passing anything outside that set is rejected:

texttext
Argument of type '"paused"' is not assignable to parameter of type 'Status'.

Unions are not limited to strings. Object types can union too, which is useful for describing a value that arrives in one of several shapes:

typescripttypescript
type ApiResponse = 
  | { data: string[]; page: number }
  | { error: string; code: number };

Working with Union Types: Narrowing

A method call is only allowed on a union value if every member of the union has that method. Calling a string-only method on a string or number value fails, because number has no such method.

typescripttypescript
function printId(id: number | string) {
  console.log(id.toUpperCase()); // Error
}

The compiler points out exactly which member of the union is missing the method, instead of waiting to fail once the code actually runs in the browser or on the server:

texttext
Property 'toUpperCase' does not exist on type 'string | number'.
  Property 'toUpperCase' does not exist on type 'number'.

Narrowing the union first solves this. TypeScript understands several narrowing constructs, starting with the most common one.

Narrow with typeof

A typeof check inside an if statement tells TypeScript which branch handles which type. Each branch then only allows operations that are valid for the narrowed type.

typescripttypescript
function printId(id: number | string) {
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // id is string here
  } else {
    console.log(id.toFixed(2));     // id is number here
  }
}

The diagram below shows the same flow: the union splits into two branches, and each branch only exposes the methods that are safe for that member.

Union narrowing flow with typeof

After the check, TypeScript knows the exact type in each branch. This narrowing is compile-time only, so nothing about the value changes at runtime, only what the compiler allows you to do with it.

Narrow with Array.isArray

The typeof operator cannot tell an array apart from a plain object, since both return "object". Array unions need Array.isArray instead.

typescripttypescript
function welcome(visitors: string[] | string) {
  if (Array.isArray(visitors)) {
    console.log("Hello, " + visitors.join(" and "));
  } else {
    console.log("Welcome, " + visitors);
  }
}

Array.isArray acts as a type guard the same way typeof does. The compiler narrows the value to a string array inside the true branch and to a plain string in the other branch.

When All Members Share a Property

If every member of a union has the same property or method, you can use it without narrowing. TypeScript only requires the operation to be valid for all members.

typescripttypescript
function getFirstThree(x: number[] | string) {
  return x.slice(0, 3); // Both arrays and strings have .slice()
}
 
console.log(getFirstThree([1, 2, 3, 4, 5])); // [1, 2, 3]
console.log(getFirstThree("TypeScript"));     // "Typ"

Both arrays and strings have a slice method, so the call is safe without narrowing at all.

Common Union Patterns

Two patterns show up constantly once you start using unions in real projects: modeling a result that can succeed or fail, and restricting a parameter to a fixed set of literal values.

typescripttypescript
type Result =
  | { ok: true; value: string }
  | { ok: false; error: string };
 
function handle(r: Result) {
  if (r.ok) {
    console.log("Got value:", r.value);
  } else {
    console.log("Error:", r.error);
  }
}

Checking the ok field narrows the result to the branch that matches, so the value field is only reachable once the call has actually succeeded. The second pattern combines a literal union with a normal string parameter:

typescripttypescript
type Alignment = "left" | "right" | "center";
 
function alignText(text: string, alignment: Alignment) {
  return `[${alignment}] ${text}`;
}
 
alignText("Hello", "left");    // "[left] Hello"
alignText("Hello", "justify"); // Error

Only "left", "right", or "center" are valid for the alignment parameter. Passing anything else, such as "justify", is rejected before the code runs:

texttext
Argument of type '"justify"' is not assignable to parameter of type 'Alignment'.

Common Union Mistakes

Trying to use a member-only method without narrowing. The length property exists on strings but not on numbers, so accessing it directly fails.

typescripttypescript
function getLength(value: string | number) {
  return value.length; // Error: 'length' does not exist on 'number'
}

The fix is the same narrowing pattern shown earlier: check the type, then use the member-specific behavior in each branch.

typescripttypescript
function getLength(value: string | number) {
  if (typeof value === "string") {
    return value.length;
  }
  return value.toString().length;
}

Forgetting that typeof null is "object". A union that mixes null with an object type needs an extra check, because JavaScript's typeof operator cannot tell them apart.

typescripttypescript
function printAll(strs: string | string[] | null) {
  if (strs && typeof strs === "object") {
    strs.forEach(s => console.log(s)); // strs is string[] here, not null
  } else if (typeof strs === "string") {
    console.log(strs);
  }
}

The truthiness check removes null first, since null is falsy, so the following typeof check only matches the array.

Using a union when a more specific pattern fits better. If you find yourself checking a shared tag property on every member, consider a discriminated union instead. It is safer and more readable.

Union types pair naturally with type narrowing. Once you understand both, you can model almost any data shape safely.

Rune AI

Rune AI

Key Insights

  • A union type is written with | and means a value can be any one of the member types.
  • You can only access properties that exist on every member of the union without narrowing.
  • Narrow a union with typeof, equality checks, or custom type guards to unlock member-specific operations.
  • Union of string literals creates a fixed set of allowed values, like "small" | "medium" | "large".
  • Always handle every member of a union to avoid runtime surprises.
RunePowered by Rune AI

Frequently Asked Questions

What is a union type in TypeScript?

A union type is a type formed from two or more other types. A value of a union type can be any one of its member types. You write it with the | (pipe) character, like string | number.

Can I access properties directly on a union type?

Only properties that exist on every member of the union. If a method exists only on one member, you must narrow the type first with typeof, instanceof, or another type guard.

What is the difference between union and intersection types?

A union (A | B) means a value is either A or B. An intersection (A & B) means a value has all properties of both A and B. Unions are for alternatives; intersections are for combining.

Conclusion

Union types are how you tell TypeScript a value can be one of several things. Define them with |, narrow them with typeof or other checks, and let the compiler guide you toward safe code. The pattern repeats everywhere in TypeScript: union first, narrow second, access safely third.