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 — preferextendsfor subtyping relationships - Verify no property conflicts would produce
nevertypes in the intersection - Prefer union
|for mutually exclusive alternatives, not intersection& - Check
readonlyand 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 Yinstead - Mutually exclusive alternatives — use union
|(string | number, notstring & 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
Determine Relationship Between Types — Classify whether the relationship is subtyping (is-a) or orthogonal composition (has-traits-of). Use
extendsfor inheritance chains and&for independent trait composition.// ✅ 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 — useextends.Validate Property Compatibility — Check each property across all participating types. The TypeScript compiler silently produces
neverwhen required properties with incompatible types intersect:// ✅ 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 --noEmitand inspect inferred types using IDE hover ortype X = ...exploration in a.tsfile. Hover over the type alias to verify no hiddenneverappears.Compose Using Intersection — Apply the
&operator with compatible types. For configuration merging patterns, intersect concrete defaults withPartial<UserConfig>: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.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
neverpattern: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)
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
// 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)
// ❌ 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)
// 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 produceneverwhich compiles but fails at runtime - Use interface
extendsfor 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
neverproduction 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
neversilently and is the #1 cause of phantom type errors - Confuse
&(intersection/composition) withextends(subtype constraint) in function signatures and generic bounds — they have fundamentally different meanings
Common Pitfalls
Silent never Type Production
// ❌ 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
// 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
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:
- Individual base types — Show each participating type/interface with its documented purpose and property list
- Compatibility analysis — Note which properties overlap between types and how conflicts (if any) are resolved
- Intersection definition — The
&composition statement with the full resulting type signature shown as a comment - Instantiation example — A concrete usage showing the combined type being constructed and accessed, proving all properties are available
- Variance check — Confirm readonly, optional, and generic variance interactions do not produce unexpected
nevertypes