Skip to content
Back to skills

Typescript Intersection

ASecurity

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

  • 4 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added June 12, 2026
developmenttypescriptgogitapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned June 12, 2026

npx -y skills add paulpas/agent-skill-router --skill typescript-intersection --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Typescript Intersection?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Typescript Intersection
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/paulpas-typescript-intersection/badge)](https://www.skillsdirectory.com/skills/paulpas-typescript-intersection)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---




name: typescript-intersection
description: Composes TypeScript types using the & intersection operator, combining interfaces, utility types, and object shapes while managing type compatibility, variance, and never-type pitfalls.
license: MIT
compatibility: opencode
metadata:
  version: "1.0.0"
  domain: coding
  triggers: typescript intersection, & type operator, intersection types, TypeScript type composition, never type conflict, TypeScript extends vs intersection, combining types
  role: implementation
  scope: implementation
  output-format: code
  content-types: [code, guidance, do-dont, examples]
  related-skills: typescript-utility-types, typescript-generics, typescript-pattern-matching
  archetypes:
    - tactical
    - generation
  anti_triggers:
    - brainstorming
    - vague ideation
    - code golf
  response_profile:
    verbosity: low
    directive_strength: high
    abstraction_level: operational




---





# 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)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.