Intermediate1 questionFull Guide

TypeScript Union & Intersection Types — Complete Interview Guide

Master TypeScript union and intersection types — how | and & work, discriminated unions for safe branching, structural composition with intersections, and the real semantic difference between OR-typed and AND-typed shapes.

The Mental Model

A union type (A | B) is a type that holds exactly one of several options at any given moment — like a box that contains either a spanner or a screwdriver, but you don't know which until you look inside. An intersection type (A & B) holds all of the options simultaneously — like a Swiss Army knife that has both a blade AND a screwdriver AND scissors all at once. When you narrow a union, you figure out which one is in the box. When you use an intersection, you expect every tool to be present immediately.

The Explanation

Union Types — A | B

A union type means a value can be one of the listed types. TypeScript only allows you to access properties that are common to all members — you must narrow before accessing member-specific properties:

type StringOrNumber = string | number;

function double(value: StringOrNumber): StringOrNumber {
  // Only properties that exist on BOTH string AND number are accessible
  // value.toString() — OK, both have toString()
  // value.toUpperCase() — Error! number has no toUpperCase()
  // value.toFixed() — Error! string has no toFixed()

  if (typeof value === "string") {
    return value.repeat(2); // Narrowed to string — string methods available
  }
  return value * 2; // Narrowed to number — arithmetic available
}

// Literal unions — a very common pattern for enumerating options
type Direction = "north" | "south" | "east" | "west";
type Status = "idle" | "loading" | "success" | "error";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";

Discriminated Unions — The Most Powerful Pattern

Add a shared literal property to every union member to create a discriminated union. TypeScript can then narrow the entire shape based on that one property's value:

// Each member has a unique 'type' literal — this is the discriminant
type Action =
  | { type: "INCREMENT"; amount: number }
  | { type: "DECREMENT"; amount: number }
  | { type: "RESET" }
  | { type: "SET_USER"; user: User };

function reducer(state: AppState, action: Action): AppState {
  switch (action.type) {
    case "INCREMENT":
      // action: { type: "INCREMENT"; amount: number }
      return { ...state, count: state.count + action.amount };
    case "RESET":
      // action: { type: "RESET" } — no amount property accessible
      return initialState;
    case "SET_USER":
      // action: { type: "SET_USER"; user: User }
      return { ...state, user: action.user };
    default:
      return state;
  }
}

// Discriminated unions also work with if/else — the discriminant can be any literal
type Shape =
  | { kind: "circle";  radius: number }
  | { kind: "square";  side: number }
  | { kind: "rect";    width: number; height: number };

function area(s: Shape): number {
  if (s.kind === "circle") return Math.PI * s.radius ** 2;
  if (s.kind === "square") return s.side ** 2;
  return s.width * s.height; // TypeScript knows kind === "rect" here
}

Intersection Types — A & B

An intersection type means a value must simultaneously satisfy all of the listed types. The result type has all properties from all members:

interface HasId   { id: number; }
interface HasName { name: string; }
interface HasTimestamps { createdAt: Date; updatedAt: Date; }

// An intersection: must have ALL properties from ALL three interfaces
type BaseEntity = HasId & HasName & HasTimestamps;

const user: BaseEntity = {
  id: 1,
  name: "Alice",
  createdAt: new Date(),
  updatedAt: new Date(),
}; // All three groups must be present

// Practical pattern: extending a third-party type without interface merging
import { Request } from "express";
type AuthRequest = Request & { user: User; sessionId: string };

function protectedHandler(req: AuthRequest, res: Response) {
  console.log(req.user.name);    // user is typed — from intersection
  console.log(req.headers.host); // headers is typed — from Request
}

Property Conflicts in Intersections

When intersected types have the same property name with incompatible types, the result is never — a silent footgun:

type A = { value: string };
type B = { value: number };
type AB = A & B;
// AB['value'] is string & number = never

const x: AB = { value: ??? }; // Impossible — nothing is both string and number
// TypeScript will not error here at definition time — the error appears when you try
// to assign a value: string is not assignable to never

Union vs Intersection — The Structural Perspective

Counterintuitively, union types have fewer members accessible (only the intersection of their property sets) while intersection types have more members accessible (the union of all property sets):

interface Cat { meow(): void; name: string; }
interface Dog { bark(): void; name: string; }

// Union Cat | Dog — only 'name' is accessible without narrowing
// (it's the only property that exists on BOTH Cat AND Dog)
function greet(pet: Cat | Dog) {
  pet.name;   // OK — both have name
  pet.meow(); // Error — Dog has no meow()
  pet.bark(); // Error — Cat has no bark()
}

// Intersection Cat & Dog — ALL properties from both are accessible
function greetSuperPet(pet: Cat & Dog) {
  pet.name;   // OK
  pet.meow(); // OK — must be a Cat
  pet.bark(); // OK — must also be a Dog
}

Common Misconceptions

⚠️

Many developers think union types allow access to all member properties — union types only allow access to properties that exist on every member. You must narrow to a specific member to access unique properties.

⚠️

Many developers think intersection types produce fewer properties than their constituents — the opposite is true. An intersection A & B has all properties from A plus all properties from B. It's wider, not narrower.

⚠️

Many developers think conflicting properties in intersections cause a compile error at definition time — TypeScript silently produces never for the conflicting property. The error only surfaces when you try to assign a value to that property.

⚠️

Many developers use discriminated unions only for error handling or state machines — they are equally valuable for any heterogeneous collection: API action types, DOM event variants, form field configurations, and router route descriptors.

⚠️

Many developers think string literal unions are just documentation — they are enforced at compile time. Passing 'norht' (typo) to a Direction parameter is a compile error, not a runtime surprise.

⚠️

Many developers think intersections and extends are exactly equivalent — interface extends errors immediately on conflicting properties. Type intersections silently produce never. This difference matters when composing types from external sources.

Where You'll See This in Real Code

Redux action types: every Redux codebase models actions as discriminated unions. Each action has a unique type literal, and reducers switch on it — TypeScript narrows to the exact action shape in each case, eliminating all action.payload as Type casts.

React component variants: a Button component might accept type: 'primary' | 'secondary' | 'danger' as a literal union prop — TypeScript enforces valid variants at every call site with no runtime cost.

Express middleware composition: AuthRequest = Request & { user: User } is the standard pattern for typed middleware that adds properties to the request — the intersection combines the library type with your application's custom additions.

API result modeling: ApiResponse<T> = { ok: true; data: T } | { ok: false; error: ApiError } is a discriminated union that forces every consumer to handle both success and failure before accessing the data.

GraphQL fragment composition: tools like GraphQL Code Generator use intersection types to model fragments — a query result type is the intersection of multiple fragment types, ensuring all queried fields are available.

Event handler unions: addEventListener callback types use union literals for the event name to provide fully typed events — 'click' gives MouseEvent, 'keydown' gives KeyboardEvent — powered by overloads and union mapping in TypeScript's DOM lib.

Interview Cheat Sheet

  • Union A | B — value is ONE of the listed types; only shared properties accessible without narrowing
  • Intersection A & B — value satisfies ALL listed types; all properties from all members are accessible
  • Literal union: type Status = 'a' | 'b' | 'c' — exhaustive compile-time enumeration of string/number literals
  • Discriminated union: shared literal property (kind, type, status) enables switch-based narrowing
  • Union property access: only properties present on ALL members are safe without narrowing
  • Intersection property conflict: same key with incompatible types → that property becomes never
  • extends vs &: extends errors on conflicts; & silently produces never
  • Use union for OR semantics (one of these), intersection for AND semantics (all of these)
  • Discriminated unions are the TypeScript-native alternative to class hierarchies for polymorphism
💡

How to Answer in an Interview

  • 1.The counterintuitive property access rule is a key differentiator: 'A union of two types has fewer accessible properties — only those common to all members. An intersection has more — everything from all members combined. It feels backwards but follows from type safety.'
  • 2.Lead discriminated union explanations with the real-world anchor: 'I model API responses, Redux actions, and state machines as discriminated unions. A shared literal property lets TypeScript narrow the entire shape — no casting, no runtime type checks beyond the one switch.'
  • 3.When asked about union vs intersection for composition, give the semantic test: 'If the value should be ONE of several things, it's a union. If the value must be ALL of several things simultaneously, it's an intersection.'
  • 4.The never footgun in intersections is a senior signal: 'Conflicting property types in an intersection silently produce never for that property — TypeScript doesn't error at definition time. This is subtle and dangerous when intersecting types from external libraries you don't control.'

Practice Questions

1 question
#01

What are union types and intersection types? When do you use each?

EasyType System💡 Union = A OR B (use narrowing to distinguish); intersection = A AND B (merged shape)

Related Topics

TypeScript Types vs Interfaces — Complete Interview Guide
Intermediate·8–12 Qs
TypeScript Utility Types — Complete Interview Guide
Intermediate·6–10 Qs
TypeScript Conditional Types — Complete Interview Guide
Advanced·6–10 Qs
TypeScript Type Guards & Narrowing — Complete Interview Guide
Intermediate·6–10 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz