One of the common core questions in a TypeScript interview is:

Explain the differences between Union (|) and Intersection (&) types in TypeScript. How do they behave when applied to primitive types vs. object types?

Unions and intersections allow you to compose complex types out of simpler types. However, their behavior changes depending on whether they are combining primitives (like strings/numbers) or object types (interfaces).


1. Union Types (|)#

A union type represents a value that can be one of several types. It is created using the pipe (|) symbol.

TYPESCRIPT
type ID = string | number;

let userId: ID;
userId = "abc"; // ✅ Allowed
userId = 123;   // ✅ Allowed
userId = true;  // ❌ Compile Error: Type 'boolean' is not assignable to type 'ID'.

Union behavior on Objects:#

If you have a union of two object shapes, you can only safely access properties that are common to both types unless you perform type narrowing.

TYPESCRIPT
interface Bird {
  fly(): void;
  layEggs(): void;
}
interface Fish {
  swim(): void;
  layEggs(): void;
}

function getAnimal(): Bird | Fish { ... }
const pet = getAnimal();

pet.layEggs(); // ✅ Allowed (Common to both)
pet.swim();    // ❌ Compile Error (Only on Fish)

2. Intersection Types (&)#

An intersection type combines multiple types into one type that has all the features of each. It is created using the ampersand (&) symbol.

TYPESCRIPT
interface Serializable {
  serialize(): string;
}
interface Loggable {
  log(): void;
}

type OutputModel = Serializable & Loggable;

const model: OutputModel = {
  serialize() { return "data"; },
  log() { console.log("Logged"); }
}; // ✅ Must implement both functions!

Intersection behavior on Primitives:#

If you intersect two incompatible primitive types, you get never, because a value cannot be both a string and a number simultaneously:

TYPESCRIPT
type Impossible = string & number; // Type resolves to 'never'

3. Discriminated Unions#

A common architectural pattern in TypeScript is the Discriminated Union (also called Tagged Unions). It is used to create clear, type-safe conditional logic by adding a common literal property (the "discriminant") to each member of the union.

TYPESCRIPT
interface SuccessResponse {
  status: "success"; // Discriminant
  data: string[];
}

interface ErrorResponse {
  status: "error"; // Discriminant
  message: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.status === "success") {
    // TypeScript knows it is SuccessResponse here
    console.log(response.data);
  } else {
    // TypeScript knows it is ErrorResponse here
    console.log(response.message);
  }
}

Key Contrast Summary#

Behavior FeatureUnion (|)Intersection (&)
Logic Meaninglogical OR (either type A OR B)logical AND (both type A AND B)
PrimitivesCreates a choice (string | number)Creates never (string & number)
Object PropertiesOnly common properties are accessibleAll properties of both types are accessible
Common Use CaseRepresenting alternative statesExtending/combining object shapes

Key Takeaways#

  • Union Types: Created using | to allow a value to be one of several types, requiring narrowing to access non-common properties.
  • Intersection Types: Created using & to combine multiple types into a single type containing all members.
  • Primitive Intersection: Intersecting incompatible primitive types like string & number results in the never type.
  • Discriminated Unions: Leverage a literal discriminant property to perform clean, type-safe conditional logic.