Oasis's Cloud

一个人的首要责任,就是要有雄心。雄心是一种高尚的激情,它可以采取多种合理的形式。
—— 《一个数学家的辩白》

子类型

作者:oasis


子类型定义:如果在期望类型 T 的实例的任何地方,都可以安全地使用类型 S 的实例,那么称类型 S 是类型 T 的子类型。

TypeScript 中是结构子类型,即只要某个类型包含另一个类型声明的所有成员,那么前者的实例就可以代替后者的实例使用。换句话说,如果一个类型的结构与另一个类型相似(具有相同成员,可能还有额外的成员)则它将自动被视为后者的子类型。

子类型的基本概念

子类型关系是类型系统中一个重要的概念。如果类型 S 是类型 T 的子类型(记作 S <: T),那么在任何需要 T 类型值的地方,都可以安全地使用 S 类型的值。

// 基本例子
type Animal = {
    name: string
    age: number
}

type Dog = {
    name: string
    age: number
    breed: string  // Dog 有额外的属性
}

// Dog 是 Animal 的子类型
function greet(animal: Animal): void {
    console.log(`Hello, ${animal.name}`)
}

const dog: Dog = { name: 'Buddy', age: 3, breed: 'Golden Retriever' }
greet(dog)  // ✅ Dog 可以安全地用作 Animal

结构子类型 vs 名义子类型

结构子类型(Structural Typing)

TypeScript 使用结构子类型系统,它关注的是类型的”形状”(结构),而不是类型的名称。只要两个类型具有兼容的结构,它们就被认为是兼容的。

// 结构子类型:只要结构匹配即可
interface Point {
    x: number
    y: number
}

interface NamedPoint {
    x: number
    y: number
    name: string
}

function printPoint(point: Point): void {
    console.log(`(${point.x}, ${point.y})`)
}

const namedPoint: NamedPoint = { x: 1, y: 2, name: 'Origin' }
printPoint(namedPoint)  // ✅ 结构匹配,可以安全使用

// 即使类型名称不同,只要结构兼容就可以使用
class MyPoint {
    x: number
    y: number
    constructor(x: number, y: number) {
        this.x = x
        this.y = y
    }
}

const myPoint = new MyPoint(3, 4)
printPoint(myPoint)  // ✅ 类实例也可以,因为结构匹配

名义子类型(Nominal Typing)

在名义类型系统中(如 Java、C#),类型兼容性基于类型的名称和显式声明。TypeScript 默认不使用名义类型,但可以通过一些技巧实现:

// 使用 unique symbol 实现名义类型
declare const PointType: unique symbol
class Point {
    [PointType]: void
    x: number
    y: number
    constructor(x: number, y: number) {
        this.x = x
        this.y = y
    }
}

declare const NamedPointType: unique symbol
class NamedPoint {
    [NamedPointType]: void
    x: number
    y: number
    name: string
    constructor(x: number, y: number, name: string) {
        this.x = x
        this.y = y
        this.name = name
    }
}

function printPoint(point: Point): void {
    console.log(`(${point.x}, ${point.y})`)
}

const namedPoint = new NamedPoint(1, 2, 'Origin')
// printPoint(namedPoint)  // ❌ 错误:即使结构相同,unique symbol 使其不兼容

子类型的规则

1. 属性包含规则

如果类型 S 包含类型 T 的所有属性(可能还有额外属性),那么 S 是 T 的子类型:

interface Base {
    a: string
    b: number
}

interface Extended extends Base {
    c: boolean  // 额外属性
}

// Extended 是 Base 的子类型
function useBase(base: Base): void {
    console.log(base.a, base.b)
}

const extended: Extended = { a: 'hello', b: 42, c: true }
useBase(extended)  // ✅ Extended 可以安全地用作 Base

2. 函数参数类型:逆变(Contravariance)

函数参数类型是逆变的:如果函数 (x: T) => void 可以接受类型 S 的参数,那么 S 必须是 T 的超类型(T 是 S 的子类型)。

interface Animal {
    name: string
}

interface Dog extends Animal {
    breed: string
}

// 函数参数类型是逆变的
type AnimalHandler = (animal: Animal) => void
type DogHandler = (dog: Dog) => void

// AnimalHandler 是 DogHandler 的子类型
// 因为可以处理 Animal 的函数,一定可以处理 Dog
const animalHandler: AnimalHandler = (animal: Animal) => {
    console.log(animal.name)
}

const dogHandler: DogHandler = animalHandler  // ✅ 安全

// 但反过来不行
const dogOnlyHandler: DogHandler = (dog: Dog) => {
    console.log(dog.breed)  // 需要访问 Dog 特有属性
}

// const animalHandler2: AnimalHandler = dogOnlyHandler  // ❌ 错误

3. 函数返回类型:协变(Covariance)

函数返回类型是协变的:如果函数返回类型 S,而 S 是 T 的子类型,那么该函数可以替代返回 T 的函数。

// 函数返回类型是协变的
type AnimalFactory = () => Animal
type DogFactory = () => Dog

// DogFactory 是 AnimalFactory 的子类型
// 因为返回 Dog 的函数,返回的一定是 Animal
const dogFactory: DogFactory = () => ({ name: 'Buddy', breed: 'Golden' })
const animalFactory: AnimalFactory = dogFactory  // ✅ 安全

const animalFactory2: AnimalFactory = () => ({ name: 'Generic' })
// const dogFactory2: DogFactory = animalFactory2  // ❌ 错误

4. 数组类型:协变

数组类型是协变的,但这可能导致类型安全问题:

// 数组是协变的
const dogs: Dog[] = [
    { name: 'Buddy', breed: 'Golden' },
    { name: 'Max', breed: 'Labrador' }
]

const animals: Animal[] = dogs  // ✅ TypeScript 允许

// 但这可能导致问题
animals.push({ name: 'Cat' })  // ✅ 编译通过
// 但 dogs 数组现在包含了不是 Dog 的对象!
// console.log(dogs[2].breed)  // ❌ 运行时错误

子类型的应用场景

1. 函数参数多态

子类型使得函数可以接受更具体的类型:

interface Shape {
    area(): number
}

interface Circle extends Shape {
    radius: number
    area(): number
}

interface Rectangle extends Shape {
    width: number
    height: number
    area(): number
}

function printArea(shape: Shape): void {
    console.log(shape.area())
}

const circle: Circle = { radius: 5, area: () => Math.PI * 5 * 5 }
const rectangle: Rectangle = { width: 4, height: 6, area: () => 4 * 6 }

printArea(circle)     // ✅ Circle 是 Shape 的子类型
printArea(rectangle)  // ✅ Rectangle 是 Shape 的子类型

2. 接口扩展

通过接口扩展实现子类型关系:

interface Readable {
    read(): string
}

interface Writable {
    write(content: string): void
}

interface ReadWrite extends Readable, Writable {
    // ReadWrite 同时是 Readable 和 Writable 的子类型
}

function processReadable(readable: Readable): void {
    console.log(readable.read())
}

function processWritable(writable: Writable): void {
    writable.write('Hello')
}

const rw: ReadWrite = {
    read: () => 'content',
    write: (content: string) => console.log(content)
}

processReadable(rw)  // ✅ ReadWrite 是 Readable 的子类型
processWritable(rw)  // ✅ ReadWrite 是 Writable 的子类型

3. 泛型约束

子类型关系在泛型约束中非常有用:

interface Comparable<T> {
    compareTo(other: T): number
}

// 约束 T 必须是 Comparable<T> 的子类型
function findMax<T extends Comparable<T>>(items: T[]): T | undefined {
    if (items.length === 0) return undefined

    let max = items[0]
    for (let i = 1; i < items.length; i++) {
        if (items[i].compareTo(max) > 0) {
            max = items[i]
        }
    }
    return max
}

class Person implements Comparable<Person> {
    constructor(public name: string, public age: number) {}

    compareTo(other: Person): number {
        return this.age - other.age
    }
}

const people: Person[] = [
    new Person('Alice', 30),
    new Person('Bob', 25),
    new Person('Charlie', 35)
]

const oldest = findMax(people)  // ✅ Person 满足 Comparable<Person> 约束

总结

子类型是 TypeScript 类型系统的核心概念之一:

理解子类型关系有助于编写更加类型安全和灵活的 TypeScript 代码。