# 类型推断
ts
中最重要的就是添加了对数据的类型定义,但是类型究竟是在哪里如何推论出来的了?
# 简单的类型推论
TypeScript
里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。比如:
let str = "string";
str = 123; // Type '123' is not assignable to type 'string'.
当我们在给变量赋值但是没有指定类型的时候,默认类型就是对应的数据类型。变量str
的类型被推断为字符串类型。 这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。
# 联合类型
如果数组或者对象中有多种数据类型,且没有指定类型。那么ts
会推断为是这几种数据类型的并集。又称为联合类型。如下所示:
数组
arr
没有指定类型,数组元素数据类型包括number
和string
类型。那么这个数组的类型就是(number | string[])
。联合类型可以赋值给这几种类型,但是不能是这几种以外的类型。
# 上下文类型
前面的类型推断都是根据等号右边的数据类型,来推断等号左边的变量的类型。TypeScript
类型推论也可能按照相反的方向进行,即根据等号左边去推断等号右边的类型, 这被叫做“按上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时。
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.button); //<- Error
};
这个例子会得到一个类型错误,TypeScript
类型检查器使用Window.onmousedown
函数的类型来推断右边函数表达式的类型。 因此,就能推断出mouseEvent
参数的类型了。 如果函数表达式不是在上下文类型的位置,mouseEvent
参数的类型需要指定为any
,这样也不会报错了。
# 类型兼容性
类型兼容性用于确定一个类型是否能赋值给其他类型,TypeScript
结构化类型系统的基本规则是,如果x
要兼容y
,那么y
至少具有与x
相同的属性。
interface Info {
name:string
};
let info:Info;
// info1和接口Info有相同的属性
let info1 = {
name:'hello'
};
// info2中不包含Info中的属性
let info2 = {
age:14
};
// info3中中既包含Info中的属性,还有其他的属性
let info3 = {
name:'world',
age:15
};
info = info1; // info1中包含属性name能够正常赋值
info = info2; // 报错,info2中缺少name属性
info = info3; // info3中包含name属性,能够正常赋值。
这里检查能否把info1
,info2
和info3
赋值给Info
接口类型的obj
,编译器检查Info
中的每个属性,看是否能在info1
,info2
和info3
中也找到对应属性。 在这个例子中,info1
,info2
和info3
必须包含名字是name的string类型成员。满足条件,才能赋值正确。
注意:ts中类型的兼容性是递归式的,也就是说不仅会检查第一层,会检查所有层的属性类型
# 函数的类型兼容性
在考虑函数的兼容性的时候,需要考虑更多的东西,比如参数个数,参数类型,可选参数与剩余参数。
- 参数个数
要查看函数x
是否能赋值给函数y
,首先看它们的参数列表x
的每个参数必须能在y
里找到对应类型的参数,注意的是参数的名字相同与否无所谓,只看它们的类型。而且x
的参数个数必须小于等于y
的参数个数。
示例:
let x = (a:number) => 0;
let y = (b:number,c:string) => 0;
// 函数赋值
y = x; // 正确赋值
x = y; // 无法赋值
函数x
的参数列表的每个参数都能够在函数y
中找到(x
的参数个数小于等于y
的参数个数),因此x能够正常赋值给y
。但是函数y
的参数为2个,而函数x
的参数只有一个,因此y
的参数列表中的参数,不能都在x的参数列表中找到,因此y不能赋值给x。这有点类似于我们定义了一个函数,里面有3个参数,你可以只传一个。
- 参数类型
函数的参数类型必须一致,才能够正常赋值。
let x = (a:number) => 0;
let y = (a:string) => 0;
y = x; //参数类型不一致 Types of parameters 'a' and 'a' are incompatible.
- 可选参数与剩余参数
当包含可选参数时,目标函数里没有相对应的类型也是不报错的。比如,当存在args参数时,我们可以使用任意个数的参数,只要类型符合即可。
const getSum = (arr:number[],callback:(...args:number[]) => number):number => {
return callback(...arr);
};
let res1 = getSum([1,2],(...args:number[]):number => {
return args.reduce((a,b) => a+b,0);
});
console.log(res1);
let res2 = getSum([1,2,3],(arg1,arg2,arg3):number => {
return arg1 + arg2 + arg3;
});
console.log(res2);
- 函数参数双向协变
所谓函数参数双向协变,是指函数参数中包含联合类型时如何赋值。
let x = (a:(number | string)) :void => {};
let y = (b:number):void => {};
y = x;
x = y;// error Type 'string' is not assignable to type 'number'.
函数x
中参数类型为联合类型,既可以接收string
类型,又可以接收number
,而函数y
参数只能接收number
类型,因此,函数x
可以赋值给y
,它可以接收y
的所有参数。但是y
不能赋值给x
,因为它无法接收string
类型的数据,相当于把原来的参数类型范围缩小了。
- 返回值类型
let x = ():string | number => 0;
let y = ():string => 'a';
x = y; // 可以赋值
y = x; // 不可以赋值
类型系统强制源函数y
的返回值类型必须是目标函数x
返回值类型的子类型。
# 枚举的类型兼容性
数值枚举类型与数字类型兼容,数字类型与枚举类型兼容也就是说如果枚举类型是数值类型的,那么可以直接给他赋值数字。
enum Status {
Success,
Error
};
let s = Status.Success;
s = 1; // 数字1可以看成是Status.Success类型
但是枚举类型与枚举类型之间是不兼容的。
enum Status {
Success,
Error
};
let s = Status.Success;
s = 1;
enum Progress {
Start,
End
}
s = Progress.Start; //error Type 'Progress.Start' is not assignable to type 'Status'
Progress.Start
作为另一个枚举成员的类型,不能直接看做是Status
类型。也就是说,我们不要用一个枚举成员类型来给另一个枚举成员赋值。
# 类的类型兼容性
类作为类型使用时,实际上只比较实例的属性是否符合要求,也就是类中constructor
中的属性
是否符合类型,对于通过static
定义的静态属性不会做检查。
class AnimalClass {
public static age:number;
constructor(public name:string){};
}
class PeopleClass {
public static age:string;
constructor(public name:string){};
}
class FoodClass {
public static age:number;
constructor(public name:number){};
}
// 将类作为类型使用时,实际上是将类的实例来作为类型使用。
let animals:AnimalClass;
let peop:PeopleClass;
let food:FoodClass;
animals = peop; // 虽然static定义的类型不一致,但是constructor中的实例成员一致,因此可以赋值。
animals = food; // 虽然static定义的类型一致,但是constructor中的实例成员不一致,因此不能赋值。
类的私有成员和受保护成员 类的私有成员和受保护成员会影响兼容性。当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。