TypeScript真香系列-类型推论和类型兼容性

javascript/jquery

浏览数:69

2020-5-28

前言

TypeScript真香系列的内容将参考中文文档,但是文中的例子基本不会和文档中的例子重复,对于一些地方也会深入研究。另外,文中一些例子的结果都是在代码没有错误后编译为JavaScript得到的。如果想实际看看TypeScript编译为JavaScript的代码,可以访问TypeScript的在线编译地址,动手操作,印象更加深刻。

类型推论

基础

TypeScript中的类型推论,就是当我们没有明确指定变量的类型时,TypeScript可以自动的推断出变量的数据类型。

let a = 3;
a = 4;
a = "s";  //错误,"s"和number类型不匹配

从上面的例子可以看出,当我们定义了一个变量a,然后进行赋值,TypeScript就自动给我们推断出变量a的类型。当我们再给变量a赋值为字符串的时候,就会出现代码中的错误提示。这样的写法在JavaScript中是可以的,但是在TypeScript中给我们进行了限制。

let a = {
    p: "",
    c: 0
};
a.p = "火影";
a.p = 1; //错误,1和string类型不匹配

最佳通用类型

上面的例子很简单,但是当我们定义的变量为数组这样比较复杂的类型的时候,TypeScript就会根据其中的成员来推断出最合适的通用类型:

let a = [1, 2, null];
a=["s"];  //错误,类型"string"和"number | null"不匹配

上下文类型

上面的例子都是通从右到左判出的类型,TypeScript类型推论也可能按相反的方向来推断,这被叫做“按上下文归类”,按上下文归类会发生在表达式的类型与所处的位置相关时。下面的例子是在函数这一节的:

function sum(a: number, b: number){
    return a + b;
}

我们没有指定返回值的类型,但是TypeScript自动从上到下推断出返回值的类型为

number。
let man = {
    a: 1,
    b: "james",
    play: (s: string) => {
        return s
    }
}

man.play = function (s){ 
   return s + "s"
}

类型兼容性

基础

TypeScript中的类型兼容性可以用于确定一个类型是否可以赋值给其他类型。这里要了解两个概念:

结构类型:一种只使用其成员来描述类型的方式;

名义类型:明确的指出或声明其类型,如c#,java。

TypeScript的类型兼容性就是基于结构子类型的。下面的例子:

interface IName {
    name: string;
}

class Man {
    name: string;
    constructor() {
        this.name = "鸣人";
    }
}

let p: IName;
p = new Man();
p.name;

上面的代码在TypeScript不会出错,但是在java等语言中就会报错,因为Man类没有明确的说明实现了IName 接口。可能有人会感觉上面的例子体现不了什么,那我们接下来看下面的不兼容的例子:

let man: string = "佐助";
let age: number = 20;

man = age;  // 错误,类型number和类型string不匹配
age = man;  // 错误,类型string和类型number不匹配

再看个兼容的例子:

let man: any = "佐助";
let age: any = 123
man = age;  //123

结构化

TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。如下面的例子:

interface IName { 
    name: string;
}
let x: IName;
let y = {name: "鸣人", age: 123, hero: true};
x = y;  //{name: "鸣人", age: 123, hero: true}

这里编译器检查了x中的每一个属性,看是否在y中也能找到对应的属性。而上面的 y 符合了 x 兼容的要求,即x兼容y。

interface IName { 
    name: string;
    age: number
}
let x: IName;

let z = { name: "佐助", cool: true };
x = z; // 错误

这里编译器在检查的时候,发现 z 中少了 x 中的”age”这个属性,所以 x 和 z 是不兼容的。

比较函数

参数不同

上面的例子都是一些原始类型或者对象之间的比较,现在我们看看函数之间是怎么比较的:

let x = (a: number) => 0;
let y = (b: number, c: string) => 0;

y = x; 
x = y; //错误

要看x能否赋值给y,先看x和y的参数列表。x的每个参数都必须在y里面找到对应类型的参数,只要参数类型相对应,参数名字无所谓。上面例子中x的参数都能在y中找到对应的参数,所以允许赋值,但是反过来,y就不能给x赋值。

函数参数的双向协变

双向协变包含协变和逆变。协变是指子类型兼容父类型,而逆变正好相反。

let man = (arg: string | number) : void => {};
let player = (arg: string) : void => {};

man = player;
player = man;

可选和rest参数

关于可选参数和rest参数的兼容,可以看下面的例子:

let man = (x: number, y: number) => {};
let work = (x?: number, y?: number) => {};
let play = (...args: number[]) => {};

man = work = play;
play = work = man;

函数重载

关于重载,我们先看看java中的定义:
在同一个类中,允许存在一个以上的同名函数,只要他们的参数个数或者参数类型不同即可。与返回值类型无关,只看参数列表(参数的个数、参数的类型、参数的顺序)

在TypeScript中的函数重载和java中的不同,TypeScript中的函数重载仅仅是参数类型重载:

function sum(a: number, b: number): number;
function sum(a: string, b: string): string;

function sum(a: any, b: any) {
    let result = null;
    if (typeof a === "string" && typeof b === "string") {
        result = <string>a + "和" + <string>b + "是好基友";
    } else if (typeof a === "number" && typeof b === "number") {
        result = <number>a + <number>b
    }
    return result;
}
sum("鸣人", "佐助");
sum(1, 1);

对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名,下面这种方式就是错误的:

function sum(a: number, b: number): number; // 错误,重载签名与其实现签名不兼容
function sum(a: string, b: string): string{
    return a + b;
};

下面这个例子在TypeScript也不能进行重载:

function sum(a: number, b: number): number{ //错误,函数重复实现
    return a + b;
};
function sum(a: any, b: any): any{ //错误,函数重复实现
    return a + b;
};

返回值不同

然后我们看看返回值类型怎么比较的,源函数的返回类型必须是目标函数返回值的子类型:

interface IMan {
  x: string;
  y: number;
}
interface IPlayer {
  x: string;
  y: number;
  z: number;
}
 
let man = (): IMan => ({ x: "鸣人", y: 0 });
let player = (): IPlayer => ({ x: "佐助", y: 0, z: 0 });
 
man = player;
player = man; //错误

从上面可以看出,player是man的子类型,所以man兼容player。下面这个例子也体现了这一点:

interface IMan {
  x: string;
  y: number;
}
interface IPlayer {
  a: string;
  b: number;
  c: number;
}
 
let man = (): IMan => ({ x: "鸣人", y: 0 });
let player = (): IPlayer => ({ a: "佐助", b: 0, c: 0 });
 
man = player; //错误
player = man; //错误

枚举

枚举类型和数字类型相互兼容:

enum Man {
    name,
    age,
}

let num = 1;
let num2 = 2;
let enumNum: Man.name = num; 
num2 = Man.name;

不同枚举之间是不兼容的:

enum Man {
    name,
    age,
}

enum Player { 
    name,
    age,
}
let man: Man.name = Player.name; //错误,类型Player.name不能分配给类型 Man.name
let player: Player.age = Man.age;  //错误,类型 Man.name不能分配给类型 Player.name

类的基本比较

在TypeScript中,只有实例成员和方法会被比较,静态成员和构造函数不会被比较。

class Man {
    name: string;
    constructor(arg: string,) {
        this.name = arg;
    }
    showName() {
        return this.name;
    }
}

class Player {
    static age: number;
    name: string;
    constructor(arg: string, hero: boolean) {
        this.name = arg;
    }
    showName() {
        return this.name;
    }
}

let man = new Man("佐助");
let player = new Player("鸣人", true);

man = player;
player = man;

从上面的例子可以看出,虽然两个类有着不同的构造函数和静态成员,但是他们有相同的实例成员和方法,所以他们之间是兼容的。

类的私有成员和受保护成员

类的私有成员和受保护成员的兼容性的比较规则是一样的。比较两个类的时候要分两种情况来看,当两个类是父子类,父类中有私有成员的时候,两个类是兼容的;当两个类是同级的类的时候,而且同级类中包含私有或受保护成员时,就不兼容了。看看下面的两个例子:
父子类:

class Man {
    private name: string;
    
    constructor(arg: string) {
        this.name = arg;
    }
    
}

class Player extends Man {
    
    constructor(arg: string) {
        super(arg);
    }
    
}

let man = new Man("鸣人");
let player = new Player("佐助");
//Man类和Player类是父子类,所以两个类是兼容的
man = player;
player = man;

同级类:

class Man {
    private name: string;
    constructor(arg: string) {
        this.name = arg;
    }
    
}

class Player {
    private name: string;
    constructor(arg: string) {
       this.name = arg;
    }
    
}

let man = new Man("鸣人");
let player = new Player("佐助");

man = player; // 错误,类型Player不能分配给类型Man,类型具有私有属性name的单独声明
player = man; // 错误,类型Man不能分配给类型Player,类型具有私有属性name的单独声明

泛型

TypeScript泛型的兼容性分两种情况,一种是类型参数没有被成员使用;另一种是类型参数被成员使用。
我们先看当类型参数没有被成员使用时:

interface IMan<T>{

}

let man1: IMan<number>;
let man2: IMan<string>;
man1 = man2;
man2 = man1;

当类型参数被成员使用时:

interface IMan<T>{
    name: T;
}

let man1: IMan<number>;
let man2: IMan<string>;
man1 = man2;  //错误,IMan<string>不能分配给IMan<number>
man2 = man1;  //错误,IMan<number>不能分配给IMan<string>


interface IMan<T>{
    name: T;
}

let man1: IMan<number>;
let man2: IMan<number>;
man1 = man2;  
man2 = man1;

在TypeScript的泛型中,如果类型参数没有被成员使用时,对兼容性没有影响;如果参数被成员使用,则会影响兼容性。

参考

https://github.com/zhongsp/Ty…
https://github.com/jkchao/typ…

最后

文中有些地方可能会加入一些自己的理解,若有不准确或错误的地方,欢迎指出~

作者:xmanlin