TS系列篇|类型的兼容性 & 类型的保护
"不畏惧,不将就,未来的日子好好努力"——大家好!我是小芝麻😄
一、类型兼容性
当一个类型 Y 可以赋值给另一个类型 X 时, 我们就可以说类型 X 兼容类型
X 兼容 Y:X(目标类型)= Y(源类型)
简单一句话概括兼容性: 重新赋值不报错(类型自动转化)
interface Named {
name: string;
}
class Person {
name: string;
constructor(name: string){
this.name = name
}
}
let p: Named;
p = new Person('金色小芝麻');
// 虽然 Person 类没有明确说明其实现了 Named 接口。但是依然能够编译成功,这就是兼容性
TypeScript
结构化类型系统的基本规则是,如果x
要兼容y
,那么y
至少具有与x
相同的属性。比如:
interface Named {
name: string;
}
let x: Named;
let y = { name: 'Alice', location: 'Seattle' };
x = y;
这里要检查y
是否能赋值给x
,编译器检查x
中的每个属性,看是否能在y
中也找到对应属性。 在这个例子中,y
必须包含名字是name
的string
类型成员。y
满足条件,因此赋值正确。
1、接口的兼容性
- 如果传入的变量和声明的类型不匹配, TS 就会进行兼容性检查
- 原理是
Duck-Check
, 就是说只要目标类型中声明的属性变量在源类型中都存在就是兼容的 多了可以,少了不行
interface X {
a: any;
b: any
}
interface Y {
a: any;
b: any;
c: any
}
let x: X = { a: 1, b: 2 }
let y: Y = { a: 1, b: 2, c: 3 }
x = y // YES
y = x // ERROR 类型 "X" 中缺少属性 "c",但类型 "Y" 中需要该属性。
2、函数的兼容性
interface Named {
name: string;
}
let y = { name: 'Alice', location: 'Seattle' };
function greet(n: Named) {
console.log('Hello, ' + n.name);
}
greet(y);
这个比较过程是递归进行的,检查每个成员及子成员。
2.1 比较参数
- 函数类型的兼容性判断,要查看 x 是否能赋值给 y,首先看它们的参数列表。
- x 的每个参数必须能在 y 里找到对应类型的参数, 注意的是参数的名字相同与否无所谓,只看它们的类型。 参数可以少但是不能多
2.1.1 固定参数
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // YES
x = y; // Error 不能将类型“(b: number, s: string) => number”分配给类型“(a: number) => number”。
2.1.2 可选参数和剩余参数
当函数的参数中出现了可选参数时: "strictNullChecks": false 情况下不抛错 反之 抛出异常
let a = (x: number, y: number) => {};
let b = (x?: number, y?: number) => {};
let c = (...args: number[]) => {};
// strictNullChecks": true 时
a = b // 固定参数可以兼容可选参数
a = c // 固定参数可以兼容剩余参数
b = c // 可选参数不兼容剩余参数
b = a // 可选参数不兼容固定参数
c = a // 剩余参数可以兼容固定参数
c = b // 剩余参数可以兼容可选参数
原因就是可选类型的参数可能为 undefined
,在strictNullChecks": true
情况下不能与 number
兼容。
2.2 比较返回值
返回值类型必须是目标函数返回值类型的子类型。(少了不行,多了可以)
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});
x = y; // YES
y = x; // Error, 不能将类型“() => { name: string; }”分配给类型“() => { name: string; location: string; }”。
// 类型 "{ name: string; }" 中缺少属性 "location",但类型 "{ name: string; location: string; }" 中需要该属性。
y 的返回中 必须有 location 属性 是字符串类型,但是 x 的返回值没有,所以 把 x 赋值给 y 的时候会抛出异常
3、类的兼容性
- 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。
class Animal {
feet: number;
constructor(name: string, numFeet: number) { }
}
class Size {
feet: number;
constructor(numFeet: number) { }
}
let a: Animal;
let s: Size;
a = s; // YES
s = a; // YES
- 类的私有成员和受保护成员会影响兼容性。
- 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。
- 允许子类赋值给父类,但是不能赋值给其它有同样类型的类。
4、泛型的兼容性
泛型在判断兼容性的时候会先判断具体的类型,然后再进行兼容性判断
- 接口内容为空没有用到泛型的时候是可以的
interface Empty<T> {}
let x!: Empty<string>
let y!: Empty<number>
x = y // YES
对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any
比较。然后用结果类型进行比较
- 接口内容不为空的时候不可以
interface noEmpty<T> {
data: T
}
let x!: noEmpty<string>
let y!: noEmpty<number>
x = y // ERROR 不能将类型“noEmpty<number>”分配给类型“noEmpty<string>”。
- 实现原理如下,先判断具体的类型在判断兼容性
interface noEmptyString {
data: string
}
interface noEmptyNumber {
data: number
}
// 前一个 data 是 字符串类型,后一个是 数字类型,所以 不能兼容
5、枚举的兼容性
- 枚举类型与数字类型兼容,并且数字类型与枚举类型兼容
- 不同枚举类型之间是不兼容的
// 数字可以赋给枚举
enum Colors {
Red,
Yellow,
}
let c: Colors
c = Colors.Red // 0
c = 1 // YES
c = '1' // ERROR
// 枚举值可以赋给数字
let n: number
n = 1
n = Colors.Red // YES
二、类型的保护
TypeScript 能够在特定的区块中保证变量属于某种确定的类型。
可以在此区块中放心的引用此类型的属性,或者调用此类型的方法
enum Type { live, machinery }
class Person {
person: string
helloPerson() {
console.log('Hello Person')
}
}
class Robot {
robot: string
helloRobot() {
console.log('Hello Robot')
}
}
function getPerson(type: Type) {
let isPerson = type === Type.live ? new Person : new Robot
if(isPerson.helloPerson){ // ERROR 类型“Person | Robot”上不存在属性“helloPerson”。
isPerson.helloPerson() // ERROR 类型“Person | Robot”上不存在属性“helloPerson”。
}else{
isPerson.helloRobot() // ERROR 类型“Person | Robot”上不存在属性“helloRobot”。
}
}
getPerson(Type.live)
上例中 标注位置会抛错,因为此时还不确定 isPerson
到底是什么类型,根据前面说过的,这种情况我们只需要加上类型断言就可以了, 如下 就能编译成功了:
function getPerson(type: Type) {
let isPerson = type === Type.live ? new Person : new Robot
if((isPerson as Person).helloPerson){ // YES
(isPerson as Person).helloPerson() // YES
}else{
(isPerson as Robot).helloRobot() // YES
}
}
这样虽然解决了问题,但是每一处都加上类型断言,增加了工作量不说,代码的可读性也变差了;类型保护就是用来解决这类问题的
1、instanceof
类型保护
- 判断一个实例是否属于某个类
function getPerson(type: Type) {
let isPerson = type === Type.live ? new Person : new Robot
if(isPerson instanceof Person){
isPerson.helloPerson()
}else{
isPerson.helloRobot()
}
}
2、in
操作符
- 判断一个属性是否属于某个对象
function getPerson(type: Type) {
let isPerson = type === Type.live ? new Person : new Robot
if('person' in isPerson){
isPerson.helloPerson()
}else{
isPerson.helloRobot()
}
}
3、typeof
类型保护
- 判断基本类型
function double(input: string | number | boolean) {
if (typeof input === 'string') {
return input.toLocaleLowerCase()
} else if (typeof input === 'number') {
return input.toFixed(2)
} else {
return !input
}
}
4、自定义的类型保护
- TS 里的类型保护本质上就是一些表达式,他们会在运行时检查类型信息,以确保在某个作用域里的类型是符合预期的
- 要自定义一个类型保护,只需要简单地为这个类型保护定义一个函数即可,这个函数的返回值是一个类型谓词
- 类型谓词的语法为
parameterName is Type
这种形式,其中parameterName
必须是当前函数签名里的一个参数名
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(" ") + value;
}
if (isString(padding)) {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
5、链判断运算符
- 链判断运算符是一种先检查属性是否存在,在尝试访问该属性的运算符, 其符号为?.
- 如果运算符左侧的操作数?.计算为 undefined 或 null, 则表达式求值为 undefined 否则,正常触发目标属性访问,方法活函数调用。
a?.b; // 如果 a 是 null/undefined, 那么返回 undefined, 否则返回 a.b 的值
=> a == null ? undefined: a.b;
a?.[x]; // 如果 a 是 null/undefined, 那么返回 undefined, 否则返回 a[x] 的值
=> a == null ? undefined: a[x];
a?.b(); // 如果 a 是 null/undefined, 那么返回 undefined
=> a == null ? undefined: a.b(); // 如果 a.b 不是函数的话抛出类型错误,否则计算 a.b() 的结果
a?.(); // 如果 a 是 null/undefined, 那么返回 undefined
=> a == null ? undefined: a(); // 如果 a 不是函数的话抛出类型错误,否则执行 a()
参考文献
[1]. TypeScript中文网
[2]. TypeScript 入门教程
转载自:https://juejin.cn/post/7008778836150075406