TypeScript: TypeScript Cross-Types and Advanced Composition
Last updated: 2026-08-26
Intersection types use & to combine multiple types into a single type—the new type possesses all the characteristics of each of the original types. It is a core tool for composite types in TypeScript.
1. Basics of Cross Types
(1) Basic Syntax
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
let person: Person = {
name: "Charlie",
age: 20
// Both must be present name and age
};
(2) Comparison with Union Types
| Operator | Meaning | Analogy |
|---|---|---|
A & B (OR) |
Satisfies both A and B | AND (and) |
| `A | B` (Conjunction) | Satisfies A or B |
type StringOrNumber = string | number; // Joint:It could be one of them
type StringAndNumber = string & number; // Crossing:It must be both at the same time → never!
// Meaningful Crossovers——Object Type
type HasId = { id: number };
type HasName = { name: string };
type Entity = HasId & HasName; // ✅ At the same time, there are id and name
(3) Overlap Between Multiple Types
type Timestamped = { createdAt: Date; updatedAt: Date };
type SoftDeletable = { deletedAt: Date | null };
type Auditable = { createdBy: string; updatedBy: string };
type FullEntity = Timestamped & SoftDeletable & Auditable;
let article: FullEntity = {
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
createdBy: "admin",
updatedBy: "editor"
};
2. Merging Rules for Cross-Type Data
(1) Properties with the Same Name—Take the Intersection of Types
When two types have properties with the same name but are of different types, the result of the intersection is the intersection of the two types:
type A = { value: string | number };
type B = { value: string | boolean };
type C = A & B;
// C 's value Type = (string | number) & (string | boolean) = string
let c: C = { value: "hello" }; // ✅ string It is the intersection
// let c2: C = { value: 42 }; // ❌ number Not in the intersection
// let c3: C = { value: true }; // ❌ boolean Not in the intersection
(2) Properties with the same name—incompatible types result in a "never" error
When the properties of two types with the same name have no overlap, the result is never—it is impossible for such a value to exist:
type A = { id: string };
type B = { id: number };
type C = A & B;
// C 's id Type = string & number = never
// No value can be both string and number
// let c: C = { id: "x" }; // ❌ You cannot string Assigned never
never—it simply makes the type unavailable. This is the key difference between cross-types and interface extends: extends throws an error when a conflict is detected, while & cross-types silently result in never.
(3) Intersection of Function Types
When function types intersect, the parameters are taken as their intersection (which usually results in never):
type StringHandler = (value: string) => void;
type NumberHandler = (value: number) => void;
type MixedHandler = StringHandler & NumberHandler;
// MixedHandler parameters = string & number = never
// In fact, there is no value that can satisfy both signatures at the same time.
3. The Difference Between Cross-Type and interface extends
(1) Syntactic Comparison
// interface extends
interface Person {
name: string;
}
interface Employee extends Person {
employeeId: string;
}
// type Crossing
type Person2 = { name: string };
type Employee2 = Person2 & { employeeId: string };
(2) Comparison of Conflict Resolution Methods
| Scenario | interface extends | type intersection & |
|---|---|---|
| Compatibility of Attribute Types with the Same Name | ✅ Subtype Overrides Supertype | Take the Intersection |
| Incompatible property types with the same name | ❌ Compilation error | Silently generates a never |
| Multiple inheritance | Single inheritance only | Multiple cross-inheritance allowed |
| Statement Merged | Support | Do Not Support |
(3) Selection Recommendations
// Use extends scenario — Compile-time conflict detection is required
interface BaseConfig {
host: string;
port: number;
}
interface DevConfig extends BaseConfig {
debug: boolean; // ✅ New Properties
// host: number; // ❌ Compilation error——Parent Type host: string Conflict
}
// Using Intercut Scenes——Requires flexible combinations of multiple types
type WithTimestamps = { createdAt: Date; updatedAt: Date };
type WithAudit = { createdBy: string; updatedBy: string };
type FullRecord = BaseConfig & WithTimestamps & WithAudit;
// Quick Combinations,No need to define an intermediate step interface
4. Common Patterns for Type Combinations
(1) Pattern 1: Mixin
"Mixing in" additional capabilities into an object using interface types:
type WithId = { id: number };
type WithTimestamps = { createdAt: Date; updatedAt: Date };
type WithSoftDelete = { deletedAt: Date | null };
type WithAudit = { createdBy: string; updatedBy: string };
// Freely combine different abilities
type BaseEntity = WithId & WithTimestamps;
type FullEntity = WithId & WithTimestamps & WithSoftDelete & WithAudit;
interface Article extends BaseEntity {
title: string;
content: string;
}
let article: Article = {
id: 1,
createdAt: new Date(),
updatedAt: new Date(),
title: "TypeScript Getting Started",
content: "TypeScript is a superset of JavaScript..."
};
(2) Model 2: Combination of Conditions
Select whether to cross a specific type based on the conditions:
type EntityWithOptional<T, TExtra> = T & Partial<TExtra>;
interface User {
id: number;
name: string;
}
interface UserProfile {
avatar: string;
bio: string;
}
// User + Optional Profile
type UserWithOptionalProfile = EntityWithOptional<User, UserProfile>;
// { id: number; name: string; avatar?: string; bio?: string }
(3) Model Three: Branded Types
Use cross-types to "label" primitive types and prevent their misuse:
type USD = number & { __brand: "USD" };
type EUR = number & { __brand: "EUR" };
function createUSD(amount: number): USD {
return amount as USD;
}
function createEUR(amount: number): EUR {
return amount as EUR;
}
let price: USD = createUSD(100);
let cost: EUR = createEUR(80);
// price = cost; // ❌ EUR Cannot be assigned to USD
// price + cost; // ❌ Cannot be combined
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD; // Calculations can be made within the same currency
}
let total = addUSD(price, createUSD(50));
console.log(total); // 150
▶ Example: Type-Safe Configuration Combinations
Output:
Development: localhost:3000 (mock: true)
Production: api.example.com:443 (ssl: true)
// Basic Configuration
type BaseConfig = {
host: string;
port: number;
};
// Additional Development Environment Configuration
type DevConfig = BaseConfig & {
debug: true;
mockApi: boolean;
};
// Additional Configuration for the Production Environment
type ProdConfig = BaseConfig & {
debug: false;
ssl: boolean;
maxConnections: number;
};
function createDevConfig(): DevConfig {
return { host: "localhost", port: 3000, debug: true, mockApi: true };
}
function createProdConfig(): ProdConfig {
return { host: "api.example.com", port: 443, debug: false, ssl: true, maxConnections: 100 };
}
let dev = createDevConfig();
let prod = createProdConfig();
console.log(`Development:${dev.host}:${dev.port} (mock: ${dev.mockApi})`);
console.log(`Production:${prod.host}:${prod.port} (ssl: ${prod.ssl})`);
Output:
Development:localhost:3000 (mock: true)
Production:api.example.com:443 (ssl: true)
5. Advanced Techniques for Cross-Type Operations
(1) Recursive Type Conversion
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends Array<any>
? T[K]
: DeepPartial<T[K]>
: T[K];
};
interface Config {
server: { host: string; port: number };
database: { url: string; pool: { min: number; max: number } };
}
type PartialConfig = DeepPartial<Config>;
let cfg: PartialConfig = {}; // All levels are available
(2) Intersections in Generic Functions
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
let person = merge({ name: "Charlie" }, { age: 20 });
// person Type:{ name: string } & { age: number }
(3) Intersections in Conditional Types
type AddTimestamps<T> = T & { createdAt: Date; updatedAt: Date };
interface User {
name: string;
email: string;
}
type TimestampedUser = AddTimestamps<User>;
// { name: string; email: string; createdAt: Date; updatedAt: Date }
▶ Example: Utility Types Built with Intersection
Output:
Development: localhost:3000 (mock: true)
Production: api.example.com:443 (ssl: true)
type WithId = { id: number };
type WithTimestamps = { createdAt: Date; updatedAt: Date };
type WithSoftDelete = { deletedAt: Date | null };
type Entity = WithId & WithTimestamps & WithSoftDelete;
type Creatable<T> = Omit<T, "id" | "createdAt" | "updatedAt" | "deletedAt">;
type Updatable<T> = Partial<Omit<T, "id">>;
interface Product extends Entity {
name: string;
price: number;
}
type CreateProduct = Creatable<Product>;
// { name: string; price: number }
type UpdateProduct = Updatable<Product>;
// { name?: string; price?: number; createdAt?: Date; ... }
let newProduct: CreateProduct = { name: "Widget", price: 9.99 };
let update: UpdateProduct = { price: 12.99 };
console.log(update.price); // 12.99
Output:
Development: localhost:3000 (mock: true)
Production: api.example.com:443 (ssl: true)
▶ Example: Branded Types for Domain Safety
Output:
Development: localhost:3000 (mock: true)
Production: api.example.com:443 (ssl: true)
type UserId = number & { __brand: "UserId" };
type OrderId = number & { __brand: "OrderId" };
function createUserId(n: number): UserId { return n as UserId; }
function createOrderId(n: number): OrderId { return n as OrderId; }
let uid = createUserId(42);
let oid = createOrderId(99);
// uid = oid; // ❌ OrderId is not assignable to UserId
function findUser(id: UserId): string {
return `User #${id}`;
}
console.log(findUser(uid)); // "User #42"
// console.log(findUser(oid)); // ❌ prevents accidental misuse
Output:
Development: localhost:3000 (mock: true)
Production: api.example.com:443 (ssl: true)
❓ FAQ
string & number). We recommend using interface extends instead of intersections—extends throws an error immediately when there’s a conflict, making it easier to pinpoint the problem.interface extends provides compile-time conflict detection, declaration merging, and support for class implements. Cross-typing is more flexible but lacks conflict detection. We recommend using extends for combining object types, and cross-typing for simple, quick combinations.type T = (A & B) | (C & D) means "either A and B are satisfied, or C and D are satisfied." Parentheses determine precedence—& has higher precedence than |, so A & B | C & D is equal to (A & B) | (C & D).📖 Summary
- Cross Type
A & BCombines all features of multiple types—the new type must satisfy all member types simultaneously - When intersecting property types with the same name, incompatible types result in
never—TypeScript does not actively report an error interface extendsincludes conflict detection, whiletypeoffers more flexibility—choose based on the context- Common combination patterns: Mixins, conditional combinations, and branded types
- Use
& { __brand: "label" }for brand types to prevent the mixing of types that have the same structure but different meanings
📝 Exercises
- Basic Problem (Difficulty ⭐): Define two types,
WithIdandWithTimestamps, combine them using a cross-type to formBaseEntity, and create an object that satisfiesBaseEntity. - Advanced Problem (Difficulty ⭐⭐): Implement the brand types
UserId = number & { __brand: "UserId" }andOrderId = number & { __brand: "OrderId" }. Write functions to create each type of ID, and verify that they cannot be assigned to each other. - Challenge Problem (Difficulty ⭐⭐⭐): Implement the
Overwrite<T, U>tool type—override properties in T with the same name using properties from U, while preserving properties with different names. Hint: Use a mapping type + a cross type.