子类型
子类型定义:如果在期望类型 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 使用结构子类型,关注类型的结构而非名称
- 子类型关系:如果 S 是 T 的子类型,S 的实例可以在任何需要 T 的地方使用
- 协变和逆变:函数参数是逆变的,返回类型是协变的
- 实际应用:子类型使得代码更加灵活和可复用,支持多态编程
理解子类型关系有助于编写更加类型安全和灵活的 TypeScript 代码。