# 类型推断

ts中最重要的就是添加了对数据的类型定义,但是类型究竟是在哪里如何推论出来的了?

# 简单的类型推论

TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。比如:

let str = "string";
str = 123;  // Type '123' is not assignable to type 'string'.

当我们在给变量赋值但是没有指定类型的时候,默认类型就是对应的数据类型。变量str的类型被推断为字符串类型。 这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。

# 联合类型

如果数组或者对象中有多种数据类型,且没有指定类型。那么ts会推断为是这几种数据类型的并集。又称为联合类型。如下所示:

联合类型 数组arr没有指定类型,数组元素数据类型包括numberstring类型。那么这个数组的类型就是(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,info2info3赋值给Info接口类型的obj,编译器检查Info中的每个属性,看是否能在info1,info2info3中也找到对应属性。 在这个例子中,info1,info2info3必须包含名字是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中的实例成员不一致,因此不能赋值。

类的私有成员和受保护成员 类的私有成员和受保护成员会影响兼容性。当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。