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

前言
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/TypeScript
https://github.com/jkchao/typescript-book-chinese

最后
文中有些地方可能会加入一些自己的理解,若有不准确或错误的地方,欢迎指出~
————————————————
版权声明:本文为CSDN博主「XmanLinC」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_42002487/article/details/104326904

上一篇:Linux 文件与目录管理


下一篇:分类:K-近邻分类之鸢尾花数据集学习(包含数据预处理中的标准化)(环境:Pycharm)