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
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
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.
// ✅ 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.
Validate Property Compatibility — Check each property across all participating types. The TypeScript compiler silently produces never when 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 --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.
Compose Using Intersection — Apply the & operator with compatible types. For configuration merging patterns, intersect concrete defaults with Partial<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 never pattern:
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 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
// ❌ 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
never types
Live References
1---2name: typescript-intersection3description: Composes TypeScript types using the & intersection operator, combining interfaces, utility types, and object shapes while managing type compatibility, variance, and never-type pitfalls.4license: MIT5---67891011# TypeScript Intersection Type Patterns1213Composes 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.1415## TL;DR Checklist1617- [ ] Use `&` only for orthogonal/unrelated type concerns — prefer `extends` for subtyping relationships18- [ ] Verify no property conflicts would produce `never` types in the intersection19- [ ] Prefer union `|` for mutually exclusive alternatives, not intersection `&`20- [ ] Check `readonly` and optional property interactions before combining types21- [ ] Limit intersection depth to 2–3 levels maximum — create intermediate type aliases for readability2223---2425## When to Use2627Use this skill when:2829- Combining unrelated type traits into a richer composite (e.g., `Auditable & Editable & Serializable = FullRecord`)30- Merging configuration objects where defaults are combined with user-supplied partial overrides31- Composing event handler signatures that add metadata or context to existing event types32- Building enriched return types from utility functions that wrap, transform, or augment domain data33- Creating DTOs (Data Transfer Objects) that aggregate multiple source interfaces into a single response shape3435## When NOT to Use3637Avoid this skill for:3839- Subtyping relationships where one type is a specialization of another — use `interface X extends Y` instead40- Mutually exclusive alternatives — use union `|` (`string | number`, not `string & number`)41- Combining more than 3–4 types in a single intersection — the resulting type becomes unreadable and hard to debug42- Intersecting types with known property name conflicts (same key, incompatible required types) — this silently produces `never`4344---4546## Core Workflow47481. **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.49 ```typescript50 // ✅ Subtyping: AdminUser IS-A ExtendedUser which IS-A BaseUser51 interface BaseUser { id: string; name: string; email: string; }52 interface ExtendedUser extends BaseUser { role: 'admin' | 'user'; permissions: string[]; }5354 // ✅ Composition: FullRecord HAS BOTH auditing and editing traits independently55 interface Auditable { createdAt: Date; createdBy: string; }56 interface Editable { updatedAt?: Date; updatedBy?: string; }57 type FullRecord = Auditable & Editable;58 // Result: { createdAt: Date; createdBy: string; updatedAt?: Date; updatedBy?: string }5960 // ✅ Composition: Serializable has nothing to do with Auditable or Editable61 interface Serializable { toJSON(): string; fromJSON(json: string): void; }62 type PersistentRecord = FullRecord & Serializable;63 ```64 **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`.65662. **Validate Property Compatibility** — Check each property across all participating types. The TypeScript compiler silently produces `never` when required properties with incompatible types intersect:67 ```typescript68 // ✅ Compatible: same name + compatible types → result has the unified type69 type A = { id: string; status: 'active' | 'inactive'; };70 type B = { id: string; priority: number };71 type Combined1 = A & B;72 // Result: { id: string; status: 'active' | 'inactive'; priority: number }7374 // ❌ Conflict: same required property with incompatible primitive types → never75 type Bad = { x: string } & { x: number };76 // Result: { x: never } — compiles but is impossible to construct!7778 // ✅ Optional + Required conflict: required type wins, no never produced79 type Safe1 = { x: string } & { x?: number };80 // Result: { x: string } — the required `string` satisfies the optional `number` too8182 // ❌ Readonly + mutable conflict with incompatible types → readonly never83 type Bad2 = { x: string } & { readonly x: number };84 // Result: { readonly x: never } — most dangerous pattern!85 ```86 **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.87883. **Compose Using Intersection** — Apply the `&` operator with compatible types. For configuration merging patterns, intersect concrete defaults with `Partial<UserConfig>`:89 ```typescript90 interface DefaultConfig {91 timeout: number;92 retries: number;93 debug: boolean;94 logLevel: 'info' | 'warn' | 'error';95 maxConnections: number;96 }9798 interface UserConfig {99 timeout?: number;100 apiKey?: string;101 debug?: false;102 customTimeoutBehavior?: 'retry' | 'fail-fast';103 }104105 // Combine defaults with user-supplied partial config via intersection106 type AppConfig = DefaultConfig & Partial<UserConfig>;107108 function createAppConfig(overrides: UserConfig): AppConfig {109 const defaults: DefaultConfig = {110 timeout: 30,111 retries: 3,112 debug: false,113 logLevel: 'info',114 maxConnections: 100,115 };116117 // Build final config: spread defaults, then apply defined overrides118 const entries = Object.entries(overrides).filter(([, v]) => v !== undefined);119 const userPart = Object.fromEntries(entries) as Partial<UserConfig>;120121 return {122 ...defaults,123 ...userPart,124 } as AppConfig;125 }126127 // Usage — type-safe overrides enforced by the compiler128 const config: AppConfig = createAppConfig({129 timeout: 60, // overrides default 30130 apiKey: "secret-key", // adds new property from UserConfig131 debug: false, // explicit override of boolean default132 });133 // config has ALL properties: { timeout: 60, retries: 3, debug: false, logLevel: 'info',134 // maxConnections: 100, apiKey: "secret-key" }135 ```136 **Checkpoint:** Verify the combined type signature matches your mental model. Use `type Keys = keyof AppConfig;` and inspect — every property from both sides should appear.1371384. **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:139 ```typescript140 interface MutableProp { x: number; }141 interface ReadOnlyProp { readonly x: string; }142143 // ❌ DANGEROUS — Readonly wins AND types conflict → readonly never (deadlock)144 type Deadlock = MutableProp & ReadOnlyProp;145 // Result: { readonly x: never } — compiles successfully but CANNOT be instantiated!146147 // ✅ GOOD — Ensure property names do not overlap between mutable/readonly interfaces148 interface Auditable {149 readonly createdAt: Date;150 createdBy: string;151 }152 interface Editable {153 updatedAt?: Date;154 updatedBy?: string;155 }156 type FullRecord = Auditable & Editable;157 // Result: { readonly createdAt: Date; createdBy: string; updatedAt?: Date; updatedBy?: string }158 // ✅ No conflicts — different property names, no never produced159160 // ✅ GOOD — Explicitly annotate mutability when combining161 interface ReadOnlyConfig {162 readonly version: number;163 readonly buildId: string;164 }165 interface WritableDefaults {166 timeout: number;167 debug: boolean;168 }169 type StableConfig = ReadOnlyConfig & WritableDefaults;170 // Result: { readonly version: number; readonly buildId: string; timeout: number; debug: boolean }171 ```172173---174175## Implementation Patterns176177### Pattern 1: Configuration Merge (Defaults + Partial Overrides)178179```typescript180interface DefaultOptions {181 retryCount: number;182 timeoutMs: number;183 loggingLevel: 'info' | 'warn' | 'error';184 maxRetries: number;185 sslEnabled: boolean;186}187188interface UserOptions {189 retryCount?: number;190 timeoutMs?: number;191 loggingLevel?: 'debug' | 'info' | 'warn' | 'error';192 customHeader?: string;193 sslEnabled?: boolean;194}195196// Intersection of concrete defaults with partial user config197type ResolvedOptions = DefaultOptions & Partial<UserOptions>;198199function resolveUserOptions(userOpts: UserOptions): ResolvedOptions {200 const defaults: DefaultOptions = {201 retryCount: 3,202 timeoutMs: 5000,203 loggingLevel: 'info',204 maxRetries: 5,205 sslEnabled: true,206 };207208 // Extract only defined values from user config to avoid overwriting with undefined209 const validEntries = Object.entries(userOpts).filter(([, v]) => v !== undefined);210 const userPart = Object.fromEntries(validEntries) as Partial<UserOptions>;211212 return { ...defaults, ...userPart };213}214215// Type-safe usage — compiler enforces only known keys are accepted216const opts = resolveUserOptions({217 retryCount: 5, // overrides default 3218 customHeader: 'x-api-key', // adds new property from UserOptions219 loggingLevel: 'debug', // uses extended union type220});221// Inferred ResolvedOptions: { retryCount: 5, timeoutMs: 5000, loggingLevel: 'debug',222// maxRetries: 5, sslEnabled: true, customHeader: 'x-api-key' }223```224225### Pattern 2: Event Handler Composition with Enrichment226227```typescript228// Base event structure used across the application229interface BaseEvent {230 id: string;231 timestamp: number;232 source: string;233 metadata: Record<string, unknown>;234}235236// Mouse-specific event — extends base with mouse properties237interface MouseEvent extends BaseEvent {238 type: 'click' | 'dblclick' | 'contextmenu';239 x: number;240 y: number;241 button: 0 | 1 | 2;242 ctrlKey: boolean;243}244245// Keyboard-specific event — extends base with keyboard properties246interface KeyboardEvent extends BaseEvent {247 type: 'keydown' | 'keyup' | 'keypress';248 key: string;249 code: string;250 shiftKey: boolean;251 altKey: boolean;252 metaKey: boolean;253}254255// Handler metadata — orthogonal trait, unrelated to event specifics256type EnrichedEvent = BaseEvent & {257 handlerName: string;258 processedAt: number;259 processingDurationMs: number;260};261262function wrapMouseEvent(event: MouseEvent): EnrichedEvent {263 return {264 ...event,265 handlerName: 'gesture-handler',266 processedAt: Date.now(),267 processingDurationMs: 0, // populated after processing268 };269}270271function wrapKeyboardEvent(event: KeyboardEvent): EnrichedEvent {272 const start = Date.now();273 const enriched: EnrichedEvent = {274 ...event,275 handlerName: 'input-handler',276 processedAt: start,277 processingDurationMs: 0,278 };279 // Access both base and keyboard-specific properties safely280 if (enriched.shiftKey || enriched.altKey || enriched.metaKey) {281 enriched.metadata.modifiers = [282 enriched.shiftKey && 'shift',283 enriched.altKey && 'alt',284 enriched.metaKey && 'meta',285 ].filter(Boolean) as string[];286 }287 enriched.processingDurationMs = Date.now() - start;288 return enriched;289}290291// Generic dispatch — works with ANY EnrichedEvent regardless of base type292function dispatchEnriched(event: EnrichedEvent): void {293 console.log(294 `[${event.handlerName}] ${event.id} @ ${new Date(event.timestamp).toISOString()}` +295 ` (${event.processingDurationMs}ms)`296 );297}298299// Type inference preserves all properties — no casting needed300const mouseEnriched = wrapMouseEvent({301 id: 'evt-001',302 timestamp: Date.now(),303 source: 'canvas',304 metadata: {},305 type: 'click',306 x: 120,307 y: 340,308 button: 0,309 ctrlKey: false,310});311dispatchEnriched(mouseEnriched); // ✅ Full type safety312```313314### Pattern 3: Interface Extension vs Intersection (BAD vs GOOD)315316```typescript317// ❌ BAD — Using & for subtyping creates confusing and inconsistent relationships318interface Animal { name: string; }319type Dog = Animal & { bark(): void }; // Dog "is-a" Animal but uses &? Confusing.320type Cat = Animal & { meow(): void }; // Inconsistent with extends pattern elsewhere321322// ❌ BAD — Deep intersection chains are unreadable and hard to debug323type A = { a: string };324type B = { b: number };325type C = { c: boolean };326type D = { d: Date };327type E = { e: string[] };328type F = { f: Record<string, unknown> };329type SuperType = A & B & C & D & E & F; // Which properties come from where? Nightmare.330331// ✅ GOOD — Use interface extension for true subtyping relationships332interface Animal { name: string; }333interface Dog extends Animal { bark(): void; }334interface Cat extends Animal { meow(): void; }335336// ✅ GOOD — Create intermediate type aliases for readability337type CoreFeatures = A & B & C; // First grouping of related traits338type ExtendedFeatures = CoreFeatures & D & E; // Add more traits incrementally339type FinalType = ExtendedFeatures & F; // Clear hierarchy of composition340```341342### Pattern 4: Discriminated Union with Intersection (Advanced)343344```typescript345// Combining discriminated unions with intersection for rich type-safe patterns346type ActionKind = 'create' | 'update' | 'delete';347348interface CreatePayload {349 kind: 'create';350 data: Record<string, unknown>;351}352353interface UpdatePayload {354 kind: 'update';355 id: string;356 data: Partial<Record<string, unknown>>;357}358359interface DeletePayload {360 kind: 'delete';361 id: string;362 softDelete?: boolean;363}364365// Intersection with discriminated union — powerful composition pattern366type Action = BaseEvent & (CreatePayload | UpdatePayload | DeletePayload);367368function processAction(action: Action): string {369 switch (action.kind) {370 case 'create':371 // TypeScript narrows: action has data, kind === 'create'372 return `Created entity with ${Object.keys(action.data).length} fields`;373 case 'update':374 // TypeScript narrows: action has id and partial data375 return `Updated entity ${action.id} with ${Object.keys(action.data).length} changes`;376 case 'delete':377 // TypeScript narrows: action has id, optional softDelete378 const method = action.softDelete ? 'soft delete' : 'hard delete';379 return `${method} entity ${action.id}`;380 }381}382383// Usage — full type safety with discriminator narrowing384const createAction: Action = {385 id: 'act-001',386 timestamp: Date.now(),387 source: 'api',388 metadata: {},389 kind: 'create',390 data: { name: 'new-resource', tags: ['production'] },391};392393processAction(createAction); // ✅ TypeScript knows action.data exists here394```395396---397398## Constraints399400### MUST DO401- Always verify property compatibility before combining types with `&` — conflicting required properties silently produce `never` which compiles but fails at runtime402- Use interface `extends` for subtyping (is-a relationships) and intersection `&` for orthogonal composition (has-traits-of)403- Add JSDoc comments explaining why a particular type was composed via intersection, especially when spanning 3+ types or using advanced patterns404- Test inferred types with TypeScript IDE hover / Go-to-Definition to catch silent `never` production before it reaches production405- Use `Partial<T>` on user-facing config interfaces to prevent conflicts with required default properties406407### MUST NOT DO408- Use `&` for mutually exclusive alternatives — use union `|` instead (e.g., `'open' | 'closed'`, never `'open' & 'closed'`)409- Create intersection chains deeper than 3 levels without intermediate type aliases (`type Step1 = A & B; type Step2 = Step1 & C;`)410- Combine types with known property name conflicts (same key, incompatible required types) — this produces `never` silently and is the #1 cause of phantom type errors411- Confuse `&` (intersection/composition) with `extends` (subtype constraint) in function signatures and generic bounds — they have fundamentally different meanings412413---414415## Common Pitfalls416417### Silent never Type Production418```typescript419// ❌ DANGEROUS — Produces { x: never } which compiles but is impossible to instantiate420type NeverTrap = { x: string } & { x: number };421// NeverTrap is a valid type alias, but you CANNOT create an object of this type!422const obj: NeverTrap = { x: "hello" }; // Error: Type 'string' is not assignable to type 'never'423424// ✅ GOOD — Use union for mutually exclusive variants instead425type SafeAlternative =426 | { mode: 'text'; value: string }427 | { mode: 'numeric'; value: number };428429const textObj: SafeAlternative = { mode: 'text', value: "hello" }; // ✅ Works430const numObj: SafeAlternative = { mode: 'numeric', value: 42 }; // ✅ Works431```432433### Optional vs Required Interaction434```typescript435// Optional property does NOT conflict with required — the required type wins cleanly436interface HasOptionalX { x?: string; extra: boolean; }437interface HasRequiredX { x: number; other: string; }438439type Combined = HasOptionalX & HasRequiredX;440// Result: { x: number; extra: boolean; other: string }441// The required `number` overrides the optional `string` — no conflict, no never!442// This is safe and intentional: the combined type demands a number for `x`.443```444445### Readonly Conflicts with Type Mismatch446```typescript447interface MutableProp { value: string; }448interface ReadOnlyProp { readonly value: number; }449450type Conflict = MutableProp & ReadOnlyProp;451// Result: { readonly value: never } — both readonly modifier AND type conflict combine452// This is the MOST dangerous pattern because:453// 1. It compiles without errors (no syntax or semantic error)454// 2. You cannot construct any object of this type at runtime455// 3. The `never` may propagate silently to dependent types456457// ✅ Mitigation: Use TypeScript's `satisfies` operator or explicit checking458interface SafeMutable { value: number; } // Make types compatible first459type ConflictFree = SafeMutable & ReadOnlyProp;460// Result: { readonly value: number } — works perfectly!461```462463---464465## Output Template466467When composing types with intersection, produce:4684691. **Individual base types** — Show each participating type/interface with its documented purpose and property list4702. **Compatibility analysis** — Note which properties overlap between types and how conflicts (if any) are resolved4713. **Intersection definition** — The `&` composition statement with the full resulting type signature shown as a comment4724. **Instantiation example** — A concrete usage showing the combined type being constructed and accessed, proving all properties are available4735. **Variance check** — Confirm readonly, optional, and generic variance interactions do not produce unexpected `never` types474475---476477## Live References478479- [TypeScript Handbook — Intersection Types](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types)480- [TypeScript Deep Dive — Intersection Types](https://basarat.gitbook.io/typescript/type-system/intersectiontypes)481- [TypeScript GitHub Issues — Intersection Never Type](https://github.com/microsoft/TypeScript/issues/15340)482- [TypeScript Handbook — Type Aliases](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-aliases)483- [TypeScript Deep Dive — Utility Types](https://basarat.gitbook.io/typescript/type-system/utility-types)484- [TypeScript Deep Dive — Discriminated Unions](https://basarat.gitbook.io/typescript/type-system/discriminated-unions)