TypeScript Intersection Types

Intersection Types combine multiple types into a new type that includes the types of all members.

This is similar to multiple inheritance in object-oriented programming, allowing a type to have the characteristics of multiple types.


SVG Diagram: Intersection Types Background Title How Intersection Types Work Type A Type A (Person) name: string age: number Plus Sign & Type B Type B (Worker) company: string salary: number Arrow Merge Intersection Type A & B (Employee) name: string age: number company: string salary: number Bottom Section: Use Cases Use Cases for Intersection Types Scenario 1 Type Composition Merge characteristics of multiple types Scenario 2 Mixin Pattern Combine functionality of multiple classes Scenario 3 Replace interface inheritance More concise type composition Arrow Marker

Why Do We Need Intersection Types?

In development, a type often needs to have the characteristics of multiple types.

For example, an employee is both a Person and a Worker, and needs to have the properties of both.

Intersection types allow us to combine multiple types into one, satisfying this requirement.

Concept Explanation:Intersection types use the&symbol to connect multiple types, meaning the new type contains members of all types. This is similar to the concept of multiple inheritance.


Basic Syntax

Use the&symbol to combine multiple types.

Example

// Define the Person type
// Includes name and age
interface Person {
    name: string;
    age: number;
}

// Define the Worker type
// Includes company name and salary
interface Worker {
    company: string;
    salary: number;
}

// Use an intersection type to merge the two interfaces
// The Employee type has all properties of both Person and Worker
type Employee = Person & Worker;

// Create an object that has characteristics of both types
var employee: Employee = {
    name: "Alice",
    age: 25,
    company: "Google",
    salary: 100000
};

console.log("Employee: " + JSON.stringify(employee));

Output:

员工: {"name":"Alice","age":25,"company":"Google","salary":100000}

Explanation:An intersection typeA & Bmeans the new type has all properties of both A and B, none can be missing.


Intersection Types vs Interface Inheritance

Intersection types can replace multiple inheritance of interfaces.

Example

// Define type A
interface A {
    a: string;
}

// Define type B
interface B {
    b: number;
}

// Use an interface to inherit multiple interfaces
// Need to use extends to inherit multiple interfaces
interface AB extends A, B {
    c: boolean;
}

// Use an intersection type (more concise)
// Directly use the & symbol to combine types
type ABType = A & B & { c: boolean };

// Both methods create a type that contains all properties
var obj: ABType = { a: "hello", b: 42, c: true };
console.log("Object: " + JSON.stringify(obj));

Output:

对象: {"a":"hello","b":42,"c":true}

Comparison:Intersection types are more concise than interface inheritance, especially when you need to inherit multiple types and add extra properties.


Type Mixing (Mixin Pattern)

Using intersection types to implement the Mixin pattern allows dynamically combining class functionality.

Example

// Define a constructor type
// Accepts any arguments and returns an object
type Constructor = new (...args: any[]) => {};

// Mixin: add timestamp functionality
// Returns a new class that extends Base
function Timestamped<T extends Constructor>(Base: T) {
    return class extends Base {
        timestamp = Date.now();
    };
}

// Mixin: add serialization functionality
// Returns a new class that extends Base, including the serialize method
function Serializable<T extends Constructor>(Base: T) {
    return class extends Base {
        serialize() {
            return JSON.stringify(this);
        }
    };
}

// Base user class
class User {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}

// Combine Mixins
// Create a user class with timestamp functionality
var TimestampedUser = Timestamped(User);
// Create a user class with serialization functionality
var SerializableUser = Serializable(User);
// Combine two Mixins
var FullUser = Serializable(Timestamped(User));

// Create an instance and test
var user = new FullUser("Alice");
console.log("Timestamp: " + user.timestamp);
console.log("Serialization: " + user.serialize());

Output:

时间戳: 17134...
序列化: {"name":"Alice","timestamp":17134...}

Mixin Pattern:This is a powerful pattern that adds new functionality to existing classes without modifying them.


Intersection Types vs Union Types

Special attention must be paid to precedence when combining intersection types and union types.

Example

// Union type: can be string or number
type StringOrNumber = string | number;

// Intersection type: intersection of incompatible types
// string & number = never (no type is both string and number)
type Both = string & number;

// Define three types
type A = { a: string };
type B = { b: number };
type C = { c: boolean };

// Combine union type and intersection type
// (A | B) & C intersects each branch of the union type with C
type Combined = (A | B) & C;

// The actual result is: { a: string; c: boolean } | { b: number; c: boolean }
// That is, either A + C or B + C
var obj: Combined = { a: "hello", c: true };
console.log("Combined: " + JSON.stringify(obj));

Output:

组合: {"a":"hello","c":true}

Important:Intersecting incompatible types results in thenevertype. For example,string & numberis invalid.


Practical Intersection Types

Intersection types are often used to create utility types for type transformation.

Example

// Mapped type: make all properties optional
// Iterate over all properties of T, add ? to make them optional
type Partial<T> = { [P in keyof T]?: T[P] };

// Mapped type: make all properties required
// Iterate over all properties of T, remove ? to make them required
type Required<T> = { [P in keyof T]-?: T[P] };

// Mapped type: make all properties readonly
// Iterate over all properties of T, add readonly
type Readonly<T> = { readonly [P in keyof T]: T[P] };

// Define a config interface
interface Config {
    host: string;
    port: number;
}

// Use utility types
var partialConfig: Partial<Config> = { host: "localhost" };
var requiredConfig: Required<Config> = { host: "localhost", port: 8080 };
var readonlyConfig: Readonly<Config> = { host: "localhost", port: 8080 };

console.log("Partial: " + JSON.stringify(partialConfig));
console.log("Required: " + JSON.stringify(requiredConfig));
console.log("Readonly: " + JSON.stringify(readonlyConfig));

Output:

部分: {"host":"localhost"}
必填: {"host":"localhost","port":8080}
只读: {"host":"localhost","port":8080}

Utility Types:The TypeScript standard library provides many utility types based on intersection types and mapped types, such as Partial, Required, Readonly, etc.


Notes

  • Incompatible types:Intersecting incompatible types results in never
  • Precedence:Union types have higher precedence than intersection types
  • Method conflicts:If two types have methods with the same name, conflicts need to be handled manually

Best Practices:Intersection types are suitable for combining multiple interfaces or types. When multiple inheritance is needed, prefer intersection types over interface inheritance.


Summary

Intersection types are a powerful type composition tool in TypeScript.

  • Syntax:Use the&symbol to connect multiple types
  • Merging:The new type contains members of all types
  • Mixin:Can be used to implement functional composition for classes
  • never:Intersecting incompatible types results in the never type

Recommendation:Using intersection types wisely can create flexible type compositions, but be careful to avoid never types caused by unnecessary intersections.


Other Extensions