# Typescript Intersection

> Composes TypeScript types using the & intersection operator, combining interfaces, utility types, and object shapes while managing type compatibility, variance, and never-type pitfalls.

- Skill: `paulpas/typescript-intersection` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paulpas/typescript-intersection`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paulpas/typescript-intersection/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: paulpas (https://skillmd.com/u/paulpas)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/paulpas/typescript-intersection

---






# TypeScript Intersection Type Patterns

Composes and combines TypeScript types using the `&` intersection operator to create unified types from orthogonal concerns. When loaded, this skill makes the model design type-safe compositions — merging configuration defaults with user overrides, enriching event types with handler metadata, combining trait interfaces for rich domain models, and avoiding silent `never` type production that causes runtime failures.

## TL;DR Checklist

- [ ] Use `&` only for orthogonal/unrelated type concerns — prefer `extends` for subtyping relationships
- [ ] Verify no property conflicts would produce `never` types in the intersection
- [ ] Prefer union `|` for mutually exclusive alternatives, not intersection `&`
- [ ] Check `readonly` and optional property interactions before combining types
- [ ] Limit intersection depth to 2–3 levels maximum — create intermediate type aliases for readability

---

## When to Use

Use this skill when:

- Combining unrelated type traits into a richer composite (e.g., `Auditable & Editable & Serializable = FullRecord`)
- Merging configuration objects where defaults are combined with user-supplied partial overrides
- Composing event handler signatures that add metadata or context to existing event types
- Building enriched return types from utility functions that wrap, transform, or augment domain data
- Creating DTOs (Data Transfer Objects) that aggregate multiple source interfaces into a single response shape

## When NOT to Use

Avoid this skill for:

- Subtyping relationships where one type is a specialization of another — use `interface X extends Y` instead
- Mutually exclusive alternatives — use union `|` (`string | number`, not `string & number`)
- Combining more than 3–4 types in a single intersection — the resulting type becomes unreadable and hard to debug
- Intersecting types with known property name conflicts (same key, incompatible required types) — this silently produces `never`

---

## Core Workflow

1. **Determine Relationship Between Types** — Classify whether the relationship is subtyping (is-a) or orthogonal composition (has-traits-of). Use `extends` for inheritance chains and `&` for independent trait composition.
   ```typescript
   // ✅ Subtyping: AdminUser IS-A ExtendedUser which IS-A BaseUser
   interface BaseUser { id: string; name: string; email: string; }
   interface ExtendedUser extends BaseUser { role: 'admin' | 'user'; permissions: string[]; }

   // ✅ Composition: FullRecord HAS BOTH auditing and editing traits independently
   interface Auditable { createdAt: Date; createdBy: string; }
   interface Editable { updatedAt?: Date; updatedBy?: string; }
   type FullRecord = Auditable & Editable;
   // Result: { createdAt: Date; createdBy: string; updatedAt?: Date; updatedBy?: string }

   // ✅ Composition: Serializable has nothing to do with Auditable or Editable
   interface Serializable { toJSON(): string; fromJSON(json: string): void; }
   type PersistentRecord = FullRecord & Serializable;
   ```
   **Checkpoint:** Ask "Does type B inherit from type A, or does the result need traits from both independently?" If independent — use `&`. If specialization — use `extends`.

2. **Validate Property Compatibility** — Check each property across all participating types. The TypeScript compiler silently produces `never` when required properties with incompatible types intersect:
   ```typescript
   // ✅ Compatible: same name + compatible types → result has the unified type
   type A = { id: string; status: 'active' | 'inactive'; };
   type B = { id: string; priority: number };
   type Combined1 = A & B;
   // Result: { id: string; status: 'active' | 'inactive'; priority: number }

   // ❌ Conflict: same required property with incompatible primitive types → never
   type Bad = { x: string } & { x: number };
   // Result: { x: never } — compiles but is impossible to construct!

   // ✅ Optional + Required conflict: required type wins, no never produced
   type Safe1 = { x: string } & { x?: number };
   // Result: { x: string } — the required `string` satisfies the optional `number` too

   // ❌ Readonly + mutable conflict with incompatible types → readonly never
   type Bad2 = { x: string } & { readonly x: number };
   // Result: { readonly x: never } — most dangerous pattern!
   ```
   **Checkpoint:** Run `tsc --noEmit` and inspect inferred types using IDE hover or `type X = ...` exploration in a `.ts` file. Hover over the type alias to verify no hidden `never` appears.

3. **Compose Using Intersection** — Apply the `&` operator with compatible types. For configuration merging patterns, intersect concrete defaults with `Partial<UserConfig>`:
   ```typescript
   interface DefaultConfig {
     timeout: number;
     retries: number;
     debug: boolean;
     logLevel: 'info' | 'warn' | 'error';
     maxConnections: number;
   }

   interface UserConfig {
     timeout?: number;
     apiKey?: string;
     debug?: false;
     customTimeoutBehavior?: 'retry' | 'fail-fast';
   }

   // Combine defaults with user-supplied partial config via intersection
   type AppConfig = DefaultConfig & Partial<UserConfig>;

   function createAppConfig(overrides: UserConfig): AppConfig {
     const defaults: DefaultConfig = {
       timeout: 30,
       retries: 3,
       debug: false,
       logLevel: 'info',
       maxConnections: 100,
     };

     // Build final config: spread defaults, then apply defined overrides
     const entries = Object.entries(overrides).filter(([, v]) => v !== undefined);
     const userPart = Object.fromEntries(entries) as Partial<UserConfig>;

     return {
       ...defaults,
       ...userPart,
     } as AppConfig;
   }

   // Usage — type-safe overrides enforced by the compiler
   const config: AppConfig = createAppConfig({
     timeout: 60,           // overrides default 30
     apiKey: "secret-key",  // adds new property from UserConfig
     debug: false,          // explicit override of boolean default
   });
   // config has ALL properties: { timeout: 60, retries: 3, debug: false, logLevel: 'info',
   //                              maxConnections: 100, apiKey: "secret-key" }
   ```
   **Checkpoint:** Verify the combined type signature matches your mental model. Use `type Keys = keyof AppConfig;` and inspect — every property from both sides should appear.

4. **Handle Readonly and Variance Interactions** — Check how readonly modifiers interact across intersection members. A readonly property from one side carries through even if another side declares it mutable. Combine this with type conflicts for the most dangerous `never` pattern:
   ```typescript
   interface MutableProp { x: number; }
   interface ReadOnlyProp { readonly x: string; }

   // ❌ DANGEROUS — Readonly wins AND types conflict → readonly never (deadlock)
   type Deadlock = MutableProp & ReadOnlyProp;
   // Result: { readonly x: never } — compiles successfully but CANNOT be instantiated!

   // ✅ GOOD — Ensure property names do not overlap between mutable/readonly interfaces
   interface Auditable {
     readonly createdAt: Date;
     createdBy: string;
   }
   interface Editable {
     updatedAt?: Date;
     updatedBy?: string;
   }
   type FullRecord = Auditable & Editable;
   // Result: { readonly createdAt: Date; createdBy: string; updatedAt?: Date; updatedBy?: string }
   // ✅ No conflicts — different property names, no never produced

   // ✅ GOOD — Explicitly annotate mutability when combining
   interface ReadOnlyConfig {
     readonly version: number;
     readonly buildId: string;
   }
   interface WritableDefaults {
     timeout: number;
     debug: boolean;
   }
   type StableConfig = ReadOnlyConfig & WritableDefaults;
   // Result: { readonly version: number; readonly buildId: string; timeout: number; debug: boolean }
   ```

---

## Implementation Patterns

### Pattern 1: Configuration Merge (Defaults + Partial Overrides)

```typescript
interface DefaultOptions {
  retryCount: number;
  timeoutMs: number;
  loggingLevel: 'info' | 'warn' | 'error';
  maxRetries: number;
  sslEnabled: boolean;
}

interface UserOptions {
  retryCount?: number;
  timeoutMs?: number;
  loggingLevel?: 'debug' | 'info' | 'warn' | 'error';
  customHeader?: string;
  sslEnabled?: boolean;
}

// Intersection of concrete defaults with partial user config
type ResolvedOptions = DefaultOptions & Partial<UserOptions>;

function resolveUserOptions(userOpts: UserOptions): ResolvedOptions {
  const defaults: DefaultOptions = {
    retryCount: 3,
    timeoutMs: 5000,
    loggingLevel: 'info',
    maxRetries: 5,
    sslEnabled: true,
  };

  // Extract only defined values from user config to avoid overwriting with undefined
  const validEntries = Object.entries(userOpts).filter(([, v]) => v !== undefined);
  const userPart = Object.fromEntries(validEntries) as Partial<UserOptions>;

  return { ...defaults, ...userPart };
}

// Type-safe usage — compiler enforces only known keys are accepted
const opts = resolveUserOptions({
  retryCount: 5,                      // overrides default 3
  customHeader: 'x-api-key',          // adds new property from UserOptions
  loggingLevel: 'debug',              // uses extended union type
});
// Inferred ResolvedOptions: { retryCount: 5, timeoutMs: 5000, loggingLevel: 'debug',
//                              maxRetries: 5, sslEnabled: true, customHeader: 'x-api-key' }
```

### Pattern 2: Event Handler Composition with Enrichment

```typescript
// Base event structure used across the application
interface BaseEvent {
  id: string;
  timestamp: number;
  source: string;
  metadata: Record<string, unknown>;
}

// Mouse-specific event — extends base with mouse properties
interface MouseEvent extends BaseEvent {
  type: 'click' | 'dblclick' | 'contextmenu';
  x: number;
  y: number;
  button: 0 | 1 | 2;
  ctrlKey: boolean;
}

// Keyboard-specific event — extends base with keyboard properties
interface KeyboardEvent extends BaseEvent {
  type: 'keydown' | 'keyup' | 'keypress';
  key: string;
  code: string;
  shiftKey: boolean;
  altKey: boolean;
  metaKey: boolean;
}

// Handler metadata — orthogonal trait, unrelated to event specifics
type EnrichedEvent = BaseEvent & {
  handlerName: string;
  processedAt: number;
  processingDurationMs: number;
};

function wrapMouseEvent(event: MouseEvent): EnrichedEvent {
  return {
    ...event,
    handlerName: 'gesture-handler',
    processedAt: Date.now(),
    processingDurationMs: 0, // populated after processing
  };
}

function wrapKeyboardEvent(event: KeyboardEvent): EnrichedEvent {
  const start = Date.now();
  const enriched: EnrichedEvent = {
    ...event,
    handlerName: 'input-handler',
    processedAt: start,
    processingDurationMs: 0,
  };
  // Access both base and keyboard-specific properties safely
  if (enriched.shiftKey || enriched.altKey || enriched.metaKey) {
    enriched.metadata.modifiers = [
      enriched.shiftKey && 'shift',
      enriched.altKey && 'alt',
      enriched.metaKey && 'meta',
    ].filter(Boolean) as string[];
  }
  enriched.processingDurationMs = Date.now() - start;
  return enriched;
}

// Generic dispatch — works with ANY EnrichedEvent regardless of base type
function dispatchEnriched(event: EnrichedEvent): void {
  console.log(
    `[${event.handlerName}] ${event.id} @ ${new Date(event.timestamp).toISOString()}` +
    ` (${event.processingDurationMs}ms)`
  );
}

// Type inference preserves all properties — no casting needed
const mouseEnriched = wrapMouseEvent({
  id: 'evt-001',
  timestamp: Date.now(),
  source: 'canvas',
  metadata: {},
  type: 'click',
  x: 120,
  y: 340,
  button: 0,
  ctrlKey: false,
});
dispatchEnriched(mouseEnriched); // ✅ Full type safety
```

### Pattern 3: Interface Extension vs Intersection (BAD vs GOOD)

```typescript
// ❌ BAD — Using & for subtyping creates confusing and inconsistent relationships
interface Animal { name: string; }
type Dog = Animal & { bark(): void };        // Dog "is-a" Animal but uses &? Confusing.
type Cat = Animal & { meow(): void };         // Inconsistent with extends pattern elsewhere

// ❌ BAD — Deep intersection chains are unreadable and hard to debug
type A = { a: string };
type B = { b: number };
type C = { c: boolean };
type D = { d: Date };
type E = { e: string[] };
type F = { f: Record<string, unknown> };
type SuperType = A & B & C & D & E & F;     // Which properties come from where? Nightmare.

// ✅ GOOD — Use interface extension for true subtyping relationships
interface Animal { name: string; }
interface Dog extends Animal { bark(): void; }
interface Cat extends Animal { meow(): void; }

// ✅ GOOD — Create intermediate type aliases for readability
type CoreFeatures = A & B & C;              // First grouping of related traits
type ExtendedFeatures = CoreFeatures & D & E; // Add more traits incrementally
type FinalType = ExtendedFeatures & F;        // Clear hierarchy of composition
```

### Pattern 4: Discriminated Union with Intersection (Advanced)

```typescript
// Combining discriminated unions with intersection for rich type-safe patterns
type ActionKind = 'create' | 'update' | 'delete';

interface CreatePayload {
  kind: 'create';
  data: Record<string, unknown>;
}

interface UpdatePayload {
  kind: 'update';
  id: string;
  data: Partial<Record<string, unknown>>;
}

interface DeletePayload {
  kind: 'delete';
  id: string;
  softDelete?: boolean;
}

// Intersection with discriminated union — powerful composition pattern
type Action = BaseEvent & (CreatePayload | UpdatePayload | DeletePayload);

function processAction(action: Action): string {
  switch (action.kind) {
    case 'create':
      // TypeScript narrows: action has data, kind === 'create'
      return `Created entity with ${Object.keys(action.data).length} fields`;
    case 'update':
      // TypeScript narrows: action has id and partial data
      return `Updated entity ${action.id} with ${Object.keys(action.data).length} changes`;
    case 'delete':
      // TypeScript narrows: action has id, optional softDelete
      const method = action.softDelete ? 'soft delete' : 'hard delete';
      return `${method} entity ${action.id}`;
  }
}

// Usage — full type safety with discriminator narrowing
const createAction: Action = {
  id: 'act-001',
  timestamp: Date.now(),
  source: 'api',
  metadata: {},
  kind: 'create',
  data: { name: 'new-resource', tags: ['production'] },
};

processAction(createAction); // ✅ TypeScript knows action.data exists here
```

---

## Constraints

### MUST DO
- Always verify property compatibility before combining types with `&` — conflicting required properties silently produce `never` which compiles but fails at runtime
- Use interface `extends` for subtyping (is-a relationships) and intersection `&` for orthogonal composition (has-traits-of)
- Add JSDoc comments explaining why a particular type was composed via intersection, especially when spanning 3+ types or using advanced patterns
- Test inferred types with TypeScript IDE hover / Go-to-Definition to catch silent `never` production before it reaches production
- Use `Partial<T>` on user-facing config interfaces to prevent conflicts with required default properties

### MUST NOT DO
- Use `&` for mutually exclusive alternatives — use union `|` instead (e.g., `'open' | 'closed'`, never `'open' & 'closed'`)
- Create intersection chains deeper than 3 levels without intermediate type aliases (`type Step1 = A & B; type Step2 = Step1 & C;`)
- Combine types with known property name conflicts (same key, incompatible required types) — this produces `never` silently and is the #1 cause of phantom type errors
- Confuse `&` (intersection/composition) with `extends` (subtype constraint) in function signatures and generic bounds — they have fundamentally different meanings

---

## Common Pitfalls

### Silent never Type Production
```typescript
// ❌ DANGEROUS — Produces { x: never } which compiles but is impossible to instantiate
type NeverTrap = { x: string } & { x: number };
// NeverTrap is a valid type alias, but you CANNOT create an object of this type!
const obj: NeverTrap = { x: "hello" };   // Error: Type 'string' is not assignable to type 'never'

// ✅ GOOD — Use union for mutually exclusive variants instead
type SafeAlternative =
  | { mode: 'text'; value: string }
  | { mode: 'numeric'; value: number };

const textObj: SafeAlternative = { mode: 'text', value: "hello" };   // ✅ Works
const numObj: SafeAlternative = { mode: 'numeric', value: 42 };      // ✅ Works
```

### Optional vs Required Interaction
```typescript
// Optional property does NOT conflict with required — the required type wins cleanly
interface HasOptionalX { x?: string; extra: boolean; }
interface HasRequiredX { x: number; other: string; }

type Combined = HasOptionalX & HasRequiredX;
// Result: { x: number; extra: boolean; other: string }
// The required `number` overrides the optional `string` — no conflict, no never!
// This is safe and intentional: the combined type demands a number for `x`.
```

### Readonly Conflicts with Type Mismatch
```typescript
interface MutableProp { value: string; }
interface ReadOnlyProp { readonly value: number; }

type Conflict = MutableProp & ReadOnlyProp;
// Result: { readonly value: never } — both readonly modifier AND type conflict combine
// This is the MOST dangerous pattern because:
// 1. It compiles without errors (no syntax or semantic error)
// 2. You cannot construct any object of this type at runtime
// 3. The `never` may propagate silently to dependent types

// ✅ Mitigation: Use TypeScript's `satisfies` operator or explicit checking
interface SafeMutable { value: number; }     // Make types compatible first
type ConflictFree = SafeMutable & ReadOnlyProp;
// Result: { readonly value: number } — works perfectly!
```

---

## Output Template

When composing types with intersection, produce:

1. **Individual base types** — Show each participating type/interface with its documented purpose and property list
2. **Compatibility analysis** — Note which properties overlap between types and how conflicts (if any) are resolved
3. **Intersection definition** — The `&` composition statement with the full resulting type signature shown as a comment
4. **Instantiation example** — A concrete usage showing the combined type being constructed and accessed, proving all properties are available
5. **Variance check** — Confirm readonly, optional, and generic variance interactions do not produce unexpected `never` types

---

## Live References

- [TypeScript Handbook — Intersection Types](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types)
- [TypeScript Deep Dive — Intersection Types](https://basarat.gitbook.io/typescript/type-system/intersectiontypes)
- [TypeScript GitHub Issues — Intersection Never Type](https://github.com/microsoft/TypeScript/issues/15340)
- [TypeScript Handbook — Type Aliases](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-aliases)
- [TypeScript Deep Dive — Utility Types](https://basarat.gitbook.io/typescript/type-system/utility-types)
- [TypeScript Deep Dive — Discriminated Unions](https://basarat.gitbook.io/typescript/type-system/discriminated-unions)

