当一个类型 Y 可以被赋值给另一个类型 X 时,我们就可以说类型 X 兼容类型 Y
X 兼容 Y : X (目标类型)= Y (源类型)
口诀:
- 结构之间兼容:成员少的兼容成员多的
- 函数之间兼容:参数多的兼容参数少的
型变
为了方便理解下面的示例,这里引入一套句法:
- A <: B 指 “A 类型是 B 类型的子类型,或者为同种类型”
- A >: B 指 “A 类型是 B 类型的超类型,或者为同种类型”
型变有四种方式:
- 不变:只能是 T
- 协变:可以是 <: T
- 逆变:可以是 >: T
- 双变:可以是 <: T 或 >: T
协变
先看段代码:
type User1 = {
id?: number | string;
name: string;
};
type User2 = {
id: number;
name: string;
};
const user1: User1 = {
id: '1',
name: '金小钗1',
};
const user2: User2 = {
id: 2,
name: '金小钗2',
};
// id 的类型为:number | undefined
function foo(user: { id?: number; name: string }) {}
/*
这里报错:
Argument of type 'User1' is not assignable to parameter of type '{ id?: number; name: string; }'.
Types of property 'id' are incompatible.
Type 'string | number' is not assignable to type 'number'.
Type 'string' is not assignable to type 'number'.ts(2345)
*/
foo(user1);
foo(user2);
在上面代码中,foo(user2)中,user2的属性id类型为number,是number | undefined的子类,实参 <: 形参。
foo(user1)报错了,因为user1的属性id类型为number | string | undefined,是number | undefined的超类。
在类型上,我们说 TypeScript 对结构(对象和类)的属性类型进行了协变,所以对于foo()参数类型,传入参数的类型只能是 <: 形参的
在 TypeScript 中,每个复杂类型的成员都会进行协变,包括对象、类、数组和函数的返回值。
函数逆变
如果函数 A 的参数数量小于或等于函数 B 的参数数量,而且满足下述条件,那么函数 A 是 函数 B 的子类型:
- A 的
this类型未指定或者 >: B 的this类型 - A 的 每一个参数类型 >: B 的 参数类型
- A 的返回值类型 <: B 的返回值类型
除了返回值类型,其他跟上面协变中的实例代码都是反过来的,这就是逆变。
先看段代码,定义了几个类,它们的关系是:Crow<: Bird<:Animal。
class Animal {}
class Bird extends Animal {
chirp() {}
}
class Crow extends Bird {
caw() {}
}
function clone(f: (b: Bird) => Bird) {}
这时定义几个函数,分别来调用下clone():
function birdToBird(b: Bird): Bird {
return new Bird();
}
function birdToCrow(b: Bird): Crow {
return new Crow();
}
function birdToAnimal(b: Bird): Animal {
return new Animal();
}
clone(birdToBird);
clone(birdToCrow);
/*
Argument of type '(b: Bird) => Animal' is not assignable to parameter of type '(b: Bird) => Bird'.
Property 'chirp' is missing in type 'Animal' but required in type 'Bird'.ts(2345)
*/
clone(birdToAnimal);
birdToAnimal的返回值类型是 >: clone参数的返回值类型的。
假如clone是这么实现的,那么你就可以理解它为啥报错了:
function clone(f: (b: Bird) => Bird) {
const bird = new Bird();
const birdNew = f(bird);
// Animal 的示例中根本就没有 chirp 方法
birdNew.chirp();
}
函数返回类型的协变指一个函数是另一个函数的子类型,即一个函数的返回类型 <: 另一个函数的返回类型。
下面来看看函数参数的类型:
function birdToBird(b: Bird): Bird {
return new Bird();
}
function crowToBird(b: Crow): Bird {
return new Bird();
}
function animalToBird(b: Animal): Bird {
return new Bird();
}
clone(birdToBird);
clone(animalToBird);
/*
Argument of type '(b: Crow) => Bird' is not assignable to parameter of type '(b: Bird) => Bird'.
Types of parameters 'b' and 'b' are incompatible.
Property 'caw' is missing in type 'Bird' but required in type 'Crow'.ts(2345)
*/
clone(crowToBird);
crowToBird的参数类型为Crow,<: Bird。
结合clone()的实现,假如crowToBird()是这么实现:
function clone(f: (b: Bird) => Bird) {
const bird = new Bird();
const birdNew = f(bird);
birdNew.chirp();
}
function crowToBird(b: Crow): Bird {
// 从 clone 方法中传入的参数为 Bird 的实例对象,这个对象中根本没有 caw()
b.caw();
return new Bird();
}
clone(crowToBird);
多余属性检查
Typescript 检查一个对象是否可赋值给另一个对象类型时,也涉及到类型拓宽。
对象类型的成员会做协变。但是,如果 TypeScript 严守这个规则,而不做额外的检查,将会导致一个问题,例如:
type Options = {
baseURL:string,
cacheSize?: number,
tier?: 'prod'|'dev'
}
class API {
constructor(private options: Options){}
}
new API({
baseURL: 'https://xxx',
/*
这里报错,说
Argument of type '{ baseURL: string; tierr: string; }' is not assignable to parameter of type 'Options'.
Object literal may only specify known properties,
but 'tierr' does not exist in type 'Options'.
Did you mean to write 'tier'?(2345)
*/
tierr: 'prod'
})
在上面代码中
- 预期类型为
{ baseURL:string, cacheSize?: number, tier?: 'prod'|'dev' } - 传入类型为
{ baseURL: string, tierr: string }
传入的类型是预期类型的子类型,但是却报错了。
新鲜对象字面量类型:指的是 TypeScript 从对象字面量中推导出来的类型。
如果对象字面量有类型断言(使用as),或者把对象字面量赋值给变量,那么新鲜对象字面量类型将拓宽为常规的对象类型,也就不能称其为新鲜对象字面量类型。
TypeScript 捕获这个错误的原因是因为它做了多余属性检查,具体过程是:尝试把一个新鲜对象字面量类型 T 赋值给另一个类型 U 时,如果 T 中存在 U 没有的属性,则 TypeScript 报错。
看看下面的例子,就知道什么情况下会做多余属性检查,什么时候不会:
type Options = {
baseURL: string,
cacheSize?: number,
tier?: 'prod' | 'dev'
}
class API {
constructor(private options: Options) { }
}
// 正常行为,没有多余属性
new API({
baseURL: 'https://xxx',
tier: 'prod'
})
/*
因为传入的参数是个字面量对象,并且被 TypeScript 推断为
新鲜对象字面量类型,所以检查到了多余的属性 badTier
*/
new API({
baseURL: 'https://xxx',
// 报错
badTier: 'prod'
})
// 使用了类型断言,所以正常运行
new API({
baseURL: 'https://xxx',
badTier: 'prod'
} as Options)
// 显示注解 Options 类型,在定义的时候,就检查多余类型了
const options: Options = {
baseURL: 'https://xxx',
// 报错
badTier: 'prod'
}
new API(options)