前言
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
最后
文中有些地方可能会加入一些自己的理解,若有不准确或错误的地方,欢迎指出~