本节介绍ts中的类型推论及类型兼容性的相关内容,ts中如果没有明确的指定类型,类型推论会自动根据环境推论具体的类型;ts中类型兼容性是基于结构子类型的,是使用成员来描述类型的方式。

  1. B站视频 https://www.bilibili.com/video/BV1ye411U7aY/

  2. 西瓜视频 https://www.ixigua.com/7321896222225236514

一、类型推论

  类型推论简单来说就是在未指定类型的时候,根据已知条件自动推论类型,如下:

let a = 5;

  上述实例中变量a的类型未明确指定,但这是设置了number类型的值,ts会自动根据值的类型推论出a的类型,同样在初始化变量、成员、默认参数值及决定函数返回值时也会进行类型推论。

let num = 1;//num的类型会被推论为number
let str = ‘111’;//str的类型会被推论为string
let obj = new A{};//obj的类型会被推论为A
let arr = [];//arr的类型会被推论为any[]
let fn = ()=>{};//fn的类型会被推论为():void=>{}

  函数参数及返回值的推论示例:

let x = function(x){}//x参数的类型为any
let x = function(x=’a’){}//x的类型为string
let x = function(){
	return 12;//返回值类型为number
}
let x = function(x){
	return x;//此时返回值为any
}

1. 最佳通用类型

  当需要从几个表达式中推断类型的时候,会使用这些表达式的类型来推断出一个最合适的通用类型,如下:

let x = [0,1,null,’s’];

  此时为变量x推论类型的时候,需要考虑所有元素的类型,上述实例中有三种选择:number、null及string,计算通用类型算法时会考虑所有的候选类型,最终确定一个所有候选类型都兼容的类型,若候选类型都无法兼容的时候,将会被推论为联合类型,上述示例中number、null和string都不兼容,所以最终的类型会是:number|null|string。   由于最终的通用类型取自所有的候选类型,有时候选类型共享相同的通用类型,但没有一个类型能最为所有候选类型的类型,如下:

class A{}
class B extends A{}
class C extends A{}
class D extends A{}
let obj  =  [new B(), new C(), new D()];

  上述实例中想让obj的类型别推断为A类型,但是数组中没有对象A类型的,因此无法推断出,为了使obj的类型为A类型,需要明确的指定obj类型,如下:

let zoo: A[] = [new B(), new C(), new D()];

  如果没有找到最佳的通用类型,类型推论的结果是空对象类型:{}。此类型没有任何成员,所以试图访问其成员的时候程序会报错。

2. 上下文类型

  ts类型推论有时会按照相反的方向进行,即根据左侧的变量推断右侧的类型,这种情况被称为“按上下文归类”,在表达式的类型与所处的位置相关时会使用上下文归类,示例如下:

window.onmousedown = function(mouseEvent) {
    console.log(mouseEvent.buton);  //<- Error
};

  上述实例报错,因为ts类型检查去会使用window.onmousedown函数的类型来推断右边函表达式的类型,推断后就可以得到mouseEvent参数的类型了,如果函数表达式不是在上下文类型的位置,mouseEvent参数的类型需要指定为any,此时就不会报错,如果上下文类型表达式包含了明确的类型信息,上下文的类型将被忽略, 如下:

window.onmousedown = function(mouseEvent: any) {
    console.log(mouseEvent.buton);  //<- Now, no error is given
};

  函数表单时有明确的参数类型注解,上下文类型被忽略。在函数的参数、赋值表达式的右边、类型断言、对象成员及数组字面量和返回值语句等情况都可能使用上下文归类。上下文类型也会作为最佳通用类型的候选类型,如:

function fun():A[]{
	return [ new B(),new C(),new D()]
}

  上述示例中,最佳通用类型有B、C、D和A,此时优先级最高的是A。

二、类型兼容性

  ts中的类型兼容性是基于结构子类型的,结构类型是一种只使用其成员来描述的方式,与基于名义的方式不同,基于名义的方式类型的兼容性和等价性是通过声明的名称决定的,如下:

interface A{
	str:string
}
class B{
	str:string
}
let a:A;
a = new Person();

  上述实例中如果是使用名义类型的情况下将会报错,因为B类没有明确说明其实现了A接口。   ts的结构性子类型是根据js代码的写法来设计,因为js中匿名对象使用广泛,所以使用结构类型系统来描述类型比使用名义类型更好。

1. 类型兼容

  ts类型系统允许某些在编译阶段无法确认安全性的操作,被当做不可靠的。ts结构化类型系统的规则是如果a兼容b,则b至少具有与a相同的属性,如:

interface A{
	str:string
}
let a:A;
let b = {str:’12’,num:11}
a = b;

  此时检查b是否能赋值给a,编译器会检查a中的每个属性,看是否能在b中也找到对应的属性,检查函数参数时使用相同的规则:

function fun(a:A){
	a.name;
}
fun(b);

  上述示例中的b有额外的属性,但因为只有目标类型的成员会被检查,所以多余的属性不影响,检测过程是递归的,会检查每个成员及其子成员。

2. 函数比较

  比较原始类型或对象类型比较常见,但是比较函数的操作比较少见,js中如何判断两个函数是否是兼容的?可以看一下实例:

let a = (x:number)=>1;
let b = (y:number,z:number)=>2;
b = a;//能正常赋值
a = b;//会报异常,因为缺少对应的参数

  查看函数是否能赋值,需要看参数列表是否兼容,上述实例中a函数的每个参数都必须在b函数中找到对应的参数才能够赋值,否则无法赋值。比较参数的时候参数的名称无所谓,只看参数的类型以及数量。   赋值时可以忽略参数,上述示例中a函数只有x参数,b函数有y和z两个参数,但是a能赋值给b,因为js中函数调用时可以忽略参数,忽略的参数默认传递undefined。 函数的返回值类型比较的时候,返回值类型必须是目标函数返回值类型的子类型,示例如下:

let x = () => ({a: 'Alice'});
let y = () => ({a: 'Alice', b: 'Seattle'});
x = y; // OK
y = x; //错误,x函数的返回类型不是y函数返回类型的子类型,因为缺少对应的属性,则不能赋值

(1) 函数参数双向协变   当比较函数参数类型时,只有档案源函数能够赋值给目标函数或者反过来时才能赋值成功,但当调用者可能传入的参数是更精确类型信息的函数,调用这个传入的函数的时候却使用了不精确的类型就有可能会发生错误。

enum Type{ A, B}
interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }
function listenEvent(eventType: Event , handler: (n: Event) => void) {}
//以下调用的时候参数handler是函数,此函数的参数是Event,但是调用的时候传递的是MouseEvent,MouseEvent继承了Event,所以可以调用,这种用方式不可靠但是比较常见,但是不
listenEvent(Type.A, (e: MouseEvent) => console.log(e.x + ',' + e.y));
//以下调用handler的参数没有符合条件,但是在使用的时候通过类型断言的方式进行类型限定,但是这种方式会存在隐患,因为有可能传入的参数不是MouseEvent而是KeyEvent,那此时调用将会出错。
listenEvent(Type.A,(e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
listenEvent(Type.A, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));
// 以下调用将出错,因为handler的参数类型和声明的完全不兼容,无法调用
listenEvent(Type.A, (e: number) => console.log(e));

(2) 可选参数及剩余参数   对于函数的可选参数和剩余参数在函数进行比较的时候,可选参数与必须参数是可交换的,原函数上多余的可选参数不会造成错误,目标函数的可选参数没有对应的参数也不会造成错误。 对于函数具有剩余参数时,会被当做无限个可选参数对待。   此种规则对于类型检查可能是不太可靠,但是实际运行的过程中,可选参数一般是不要求强制传递的,对于大多数函数的可选参数来说,不传递的话就相当于传递了undefined。

function invokeLater(args: any[], callback: (...args: any[]) => void) {}
//调用的时候参数数量可能不固定,有可能没有也有可能多个
invokeLater([1, 2], (x, y) => console.log(x + ', ' + y));
// x和y设置了可选,但是实际上x和y是必须传递的
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));

(3) 函数重载   对于重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名才能兼容,确保目标函数可以在所有源函数可调用的地方也能调用。

3. 枚举兼容

  不同枚举类型之间是不兼容的,枚举类型和数字类型兼容,并且数字类型和枚举类型也是兼容的。

enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
let status = Status.Ready;
status = Color.Green;  //不同枚举类型之间不兼容
let a = 1;
let b = Status.Ready;
a = b;

4. 类兼容

  比较类类型的对象时,因为类有静态部分和实例部分,但只比较实例部分,静态成员和构造函数不进行比较。

class A{
    feet: number;
    constructor(name: string, numFeet: number) { }
}
class B{
    feet: number;
    constructor(numFeet: number) { }
}
let a: A;
let s: B;
a = s;  //OK
s = a;  //OK

  对于类的私有成员,在检查兼容性时,如果包含一个私有成员,则目标类型必须包含来自同一个类的私有成员,即允许子类赋值给父类,但不能赋值给其它同样类型的类型。

class A{
  private	a: number;
}
class B{
  private a: number;
}
class C extends A{
}
let a:A = new A();
let b:B = new B();
let c:C = new C();
a = b;//可以赋值
a = c;//错误,不可赋值,因为属性a不在同一个地方声明

5. 泛型兼容

  类型参数只影响使用其作为类型一部分的结果类型:

interface A<T>{}
let a:A<number>;
let b:A<string>;
a = b;

  上述代码中a和b是兼容的,因为他们的解构使用类型参数时没有不同,若果增加了成员,则不行,如下:

interface A<T>{
	d:T
}
let a:A<number>;
let b:A<string>;
a = b;//报错,a和b类型不兼容,因为有对应的属性,且属性的类型不一致。

  当没指定泛型的类型时,会把所有泛型参数当做any比较。

6.子类型与赋值

  ts中有两种兼容性:子类型和赋值,不同地方在于赋值扩展了子类型兼容,允许给any赋值或从any取值,允许数字赋值给枚举类型,枚举类型赋值给数字。