Tutorials Logic, IN info@tutorialslogic.com
Navigation
Home About Us Contact Us Blogs FAQs
Tutorials
All Tutorials
Services
Academic Projects Resume Writing Website Development
Practice
Quiz Challenge Interview Questions Certification Practice
Tools
Online Compiler JSON Formatter Regex Tester CSS Unit Converter Color Picker
Compiler Tools

TypeScript Union and Literal Types: Flexible but Safe Values

What Is a Union Type?

A union type allows a value to be one of several allowed types. Use the | symbol to join alternatives. Union types are useful when real data can arrive in more than one valid form.

Unions are common with route parameters, form inputs, API responses, UI states, feature flags, and values that start simple but become more specific after validation.

A union does not mean you can use every method from every member. Before using a member-specific method, narrow the value to the correct branch.

Union Type
function printId(id: string | number): void {
  if (typeof id === "string") {
    console.log(`String ID: ${id.toUpperCase()}`);
    return;
  }

  console.log(`Numeric ID: ${id.toFixed(0)}`);
}

printId(101);
printId("USR-101");

Why Narrowing Is Required

When a value is string | number, TypeScript only lets you use behavior that is safe for both strings and numbers. This prevents calling toUpperCase() on a number or toFixed() on a string.

Narrowing is the process of proving which union member you currently have. Common narrowing tools include typeof, equality checks, in, discriminated unions, and custom type guards.

CheckBest For
typeof value === "string"Primitive unions
status === "success"Literal unions
"data" in responseObject unions with different properties
isUser(value)Reusable runtime validation

Literal Types

Literal types restrict a value to exact strings, numbers, or booleans. Instead of saying a theme is any string, you can say it must be "light", "dark", or "system".

This is more precise than a plain string and catches invalid options at compile time. Literal unions are excellent for statuses, roles, modes, directions, sizes, variants, and command names.

Literal Options
type Theme = "light" | "dark" | "system";
type ButtonSize = "sm" | "md" | "lg";

function setTheme(theme: Theme): void {
  console.log(`Theme changed to ${theme}`);
}

function buttonClass(size: ButtonSize): string {
  return `btn btn-${size}`;
}

setTheme("dark");
// setTheme("blue"); // Error

Unions with null and undefined

A value that may be missing should say so in its type. With strict null checks enabled, User | null forces you to handle the missing case before using the user.

This is better than pretending a value always exists. It makes loading states, optional selections, and lookup failures visible in the code.

Nullable Union
type User = {
  id: number;
  name: string;
};

function getDisplayName(user: User | null): string {
  if (user === null) {
    return "Guest";
  }

  return user.name;
}

Object Unions

Object unions are useful when each valid shape has different properties. TypeScript will only allow access to properties that are safe for the current narrowed branch.

You can narrow object unions using the in operator, a shared literal property, or a custom type guard.

Object Union
type EmailContact = {
  email: string;
};

type PhoneContact = {
  phone: string;
};

function contactLabel(contact: EmailContact | PhoneContact): string {
  if ("email" in contact) {
    return `Email: ${contact.email}`;
  }

  return `Phone: ${contact.phone}`;
}

Discriminated Unions

A discriminated union is an object union where every branch has a shared literal property. That shared property is called the discriminator. It tells TypeScript which object shape is active.

This pattern is very useful for UI loading states, API responses, payment flows, form submission states, and any workflow where each state has different data.

StateAvailable Data
idleNo extra data needed.
loadingNo extra data yet.
successThe loaded data is available.
errorAn error message is available.
Discriminated Union
type LoadState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: string[] }
  | { status: "error"; message: string };

function render(state: LoadState): string {
  switch (state.status) {
    case "success":
      return state.data.join(", ");
    case "error":
      return state.message;
    case "loading":
      return "Loading...";
    default:
      return "Ready";
  }
}

Exhaustive Checks

An exhaustive check helps ensure every union branch is handled. If you later add a new state and forget to update the switch, TypeScript can report the missing case.

This is one of the strongest reasons to use discriminated unions. Instead of discovering a missing UI state in production, you get a compiler warning while editing.

Exhaustive Switch
function assertNever(value: never): never {
  throw new Error(`Unhandled value: ${JSON.stringify(value)}`);
}

function label(state: LoadState): string {
  switch (state.status) {
    case "idle":
      return "Idle";
    case "loading":
      return "Loading";
    case "success":
      return "Loaded";
    case "error":
      return "Failed";
    default:
      return assertNever(state);
  }
}

Union Arrays and Records

Union types can describe array values and object values too. For example, a list of roles can be typed as UserRole[], and a lookup table can use a union as its key type.

This keeps option lists, permissions, and configuration objects aligned with the exact values the application supports.

Union Collections
type UserRole = "admin" | "editor" | "viewer";

const roles: UserRole[] = ["admin", "editor", "viewer"];

const labels: Record<UserRole, string> = {
  admin: "Administrator",
  editor: "Content Editor",
  viewer: "Read Only User",
};

function labelRole(role: UserRole): string {
  return labels[role];
}

Common Union Design Tips

A good union should model real alternatives. Avoid unions that are so broad they stop being useful, such as string | number | object | any. The more precise the alternatives, the more TypeScript can help.

For object unions, prefer a clear discriminator such as type, kind, or status. This keeps narrowing simple and makes the data shape easier to read.

  • Use literal unions instead of plain strings for fixed options.
  • Use discriminated unions for state machines and API responses.
  • Narrow before using properties or methods that only exist on one branch.
  • Avoid adding any to a union because it weakens the whole type.
  • Use exhaustive checks when every branch must be handled.
  • Prefer a shared discriminator for object unions that represent states.
Key Takeaways
  • Union types allow controlled alternatives.
  • Literal types restrict values to exact allowed options.
  • Narrowing is required before using member-specific behavior.
  • Use null or undefined in a union when missing values are valid.
  • Discriminated unions model UI and API states clearly.
  • Exhaustive checks help catch missing branches when a union grows.
  • Union types can drive arrays, records, roles, statuses, and option lists.

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.