「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。
我们在一些时候可以省略类型的指定,TS 会帮我们推断出省略类型地方适合的类型。
我们通过学习类型推论,可以了解 TS 的推论规则。
类型推论就是为了适应 JS 灵活的特点,从而在一些情况下,只要兼容的类型即可通过检测。
一、简单示例
我们先来看一个最基础的示例:
let name = 'dylan';
name = 123; // Error:不能将类型“number”分配给类型“string”。
复制代码
我们定义了一个 name
属性,但没有指定是什么类型,直接给它设定了一个字符串的值,TS就会帮我们推断出 name
可能是想要一个字符串类型。所以当我们给 name
再赋一个数值类型的值时,就会报错了。
二、多类型联合
let arr = [1, 'a'];
复制代码
我们定义一个数组 arr
,其中有两个元素:数值类型 1
,以及字符串类型 a
。
TS 会将上述数组类型推断为:Array<number | string>
这个时候如果我们想给这个数组赋另外一个值(带有number、string类型之外的值),就会报错。
arr = [1, 2, 3, 'a', true]; // Error:不能将类型“boolean”分配给类型“string | number”。
复制代码
三、类型兼容性
interface Infos {
name: string;
}
let infos: Info;
const infos1 = { name: 'dylan' };
const infos2 = { age: 18 };
const infos3 = { name: 'dylan', age: 18 };
复制代码
我们定义了一个 Infos
接口、一个未赋值的参数 infos
以及三个已经赋值了的参数 infos1
、 infos2
、 infos3
接下来我们用这三个已经赋值了的对象,给 infos
赋值。
infos = infos1;
infos = infos2; // Error:类型 "{ age: number; }" 中缺少属性 "name"
infos = infos3; // 可以多,不能少
复制代码
infos1
给infos
赋值(OK):infos1
完全符合Infos
接口定义的内容infos2
给infos
赋值(Error):infos1
中缺少name
字段infos3
给infos
赋值(OK):infos3
中是有name
字段的。赋值要求:赋值的值中必须要有被赋值的值中全部要求的字段,多了无所谓。
这里的兼容性检测是深层次递归检测的。
四、函数兼容性
1、参数个数
let x = (a: number) => 0;
let y = (b: number, c: string) => 0;
复制代码
这里定义了 x、y 两个函数,这两个函数只有参数不同。
现在我们来进行赋值:
y = x; // OK
复制代码
可以看到把 x
赋值给 y
是没有问题的。
接下来我们尝试一下把 y
赋值给 x
:
y = x; // Error:不能将类型“(b: number, c: string) => number”分配给类型“(a: number) => number”。
复制代码
可以看到现在报错了。
TS关于参数个数的要求就是:右边函数的参数个数,必须小于等于左边函数的参数个数。
我们来看一个实际的例子:
const arr = [1, 2, 3];
arr.forEach((item, index, array) => {
console.log(item)
})
复制代码
先定义一个数组 arr
,调用数组的 forEach
方法。我们知道 forEach
接收一个回调函数,回调函数的参数有三个,分别代表当前项、索引、数组本身。
可以看到上述代码实际上我们只用到了第一个参数(当前项)。所以我们在一般使用到的时候一般会忽略后面两项:
arr.forEach(item => {
console.log(item);
})
复制代码
使用上述代码方式来调用 forEach
。
现在我们就可以理解之前的例子了:forEach
要求回调函数参数有3个,但我们实际用的时候只用到1个。所以我们回调函数的参数个数,一定要小于等于被赋值函数的参数个数。
2、参数类型
let x = (a: number) => 0;
let y = (b: string) => 0;
x = y; // Error:参数“b”和“a” 的类型不兼容。
复制代码
两个不同参数类型的函数,不能相互赋值:类型不兼容
3、可选参数和剩余参数
const getSum = (arr: number[], callback: (...args: number[]) => number): number => {
return callback(...arr);
};
复制代码
定义一个 getSum
函数
- 参数1是
number类型的数组
; - 参数2是一个回调函数(参数是一个
number类型的数组
,返回值是number
类型); - 返回值是
number
类型。
接下来我们调用一下 getSum
:
const result = getSum([1, 2, 3], (...args: number[]): number => {
return args.reduce((pre, cur) => pre + cur, 0);
})
console.log(result); // 6
复制代码
与上述代码相对应的就是设定好固定的参数
const result2 = getSum([1, 2, 3], (arg1: number, arg2: number, arg3: number): number => {
return arg1 + arg2 + arg3;
})
复制代码
可以看到,result
比 result2
更加灵活一些。
当要被赋值的函数参数中包含剩余参数(例如上述代码中的 ...args)的时候,赋值的函数可以用任意个数参数代替(类型需要一样)。
同时,剩余参数可以看做是无数可选参数。
4、函数参数双向协变
let funcA = (arg: number | string): void => {};
let funcB = (arg: number): void => {};
复制代码
定义了两个函数:funcA
和 funcB
,现在我们进行赋值:
funcA = funcB; // 可以
funcB = funcA; // 也可以
复制代码
两种赋值方式都是可以的。
我们可以看出 funcA
的参数类型即可以是 number
也可以是 string
;而 funcB
的参数类型是 number
。这两个函数都可以只接收一个参数(number类型)
5、返回值类型
let x = (): string | number => 0;
let y = (): string => 'a';
x = y; // OK
y = x; // Error:不能将类型“string | number”分配给类型“string”。
复制代码
我们把 y
赋值给 x
是没有问题的,因为 y
是 string
类型,x
即可以是 string
类型,也可以是 number
类型。
反过来把 x
赋值给 y
就不行了。
6、函数重载
function merge(arg1: number, arg2: number): number;
function merge(arg1: string, arg2: string): string;
function merge(arg1: any, arg2: any) { // 实际的函数体就不算作函数重载的一部分了
return arg1 + arg2;
}
复制代码
五、枚举的兼容性
数字枚举类型和数字类型是互相兼容的。
enum Status {
On,
Off,
}
let s = Status.On;
s = 2; // OK:它是和数值类型兼容的
复制代码
如果我们再定义一个枚举
enum Animal {
Dog,
Cat
}
复制代码
再对 s
进行赋值
s = Animal.Dog; // Error:不能将类型“Animal.Dog”分配给类型“Status”。
复制代码
发现报错了,虽然 Status.On
和 Animal.Dog
代表的值都是 0,但这里是不兼容的。
所以,数字枚举类型只与数字类型兼容,在不同枚举之间是不兼容的。
六、类的兼容性
只比较实例的成员。类的静态成员和构造函数不进行比较。
我们来定义三个类:
class AnimalClass {
public static age: number;
constructor(public name: string) {}
}
class PeopleClass {
public static age: string;
constructor(public name: string) {}
}
class FoodClass {
constructor(public name: number) {}
}
复制代码
定义三个变量,把上述 class
当作类型来使用:
let animal: AnimalClass
let people: PeopleClass;
let food: FoodClass;
复制代码
当使用类来指定一个变量的类型时,TS 检测的是实例
animal = people; // OK
animal = food; // Error:属性“name”的类型不兼容。
复制代码
虽然 AnimalClass
和 PeopleClass
的静态属性 age
的类型不一样,但是类作为类型时并不会检测静态成员和构造函数,只会比较实例上的成员。他俩的实例都通过 public name: string
给实例上添加了一个 name
属性,而且类型都是 string
,所以这两个类的类型是兼容的。
同理,FoodClass
的实例成员也有 name
属性,但类型是 number
,所以是不兼容的。
private、protected
使用 private、protected
这两个修饰符的话,会对类的兼容性造成影响。
当检查类的实例的兼容性时,如果目标类型(被赋值的一方)包含私有成员,那么原类型(赋值的一方)必须包含来自同一个类的私有成员。
class ParentClass {
private age: number;
constructor() {}
}
class ChildrenClass extends ParentClass {
constructor() {
super();
}
}
class OtherClass {
private age: number;
constructor() {}
}
复制代码
看类定义的话,ParentClass
和 OtherClass
是一模一样的,除了类名不同。
接下来我们创建实例测试:
const children: ParentClass = new ChildrenClass(); // OK:子类可以赋值给父类类型的值
const other: ParentClass = new OtherClass(); // Error:不能将类型“OtherClass”分配给类型“ParentClass”。类型具有私有属性“age”的单独声明。
复制代码
protected
与 private
一样
七、泛型的兼容性
泛型包含类型参数,这个类型参数可以是任意类型。
在使用时,类型参数会被指定为一个特定的类型,这个类型只影响使用了类型参数的部分
interface Data<T> {}
let data1: Data<number>;
let data2: Data<string>;
data1 = data2; // OK:因为函数块内部是空的(没有用到)
复制代码
但如果给接口定义一个实际的东西:
interface Data<T> {
data: T
}
let data1: Data<number>;
let data2: Data<string>;
data1 = data2; // Error:不能将类型“Data<string>”分配给类型“Data<number>”。
复制代码
现在赋值就会报错了,因为 data1
要求里面有个 data
属性,类型是 number
;而 data2
要求里面有个 data
属性,类型是 string
。这时他俩就是不兼容的了。