TypeScript
它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程,支持ES6。
- 编译型语言:编译为 js 后运行,单独无法运行;
- 强类型语言;
- 面向对象的语言;
优势
- 类型系统实际上是最好的文档,大部分的函数看看类型的定义就可以知道如何使用;
- 可以在编译阶段就发现大部分错误,这总比在运行时候出错好;
- 增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等;
总结:TypeSctipt增加了代码的可读性和可维护性。
1.安装
npm install -g typescript
tsc -v
2.编译代码
tsc xxx.ts
生成xxx.js
3.数据类型
3.1 boolean number string
let name: type
3.2 Array
let list: number[] = [1, 2, 3];
//或
let list: Array<number> = [1, 2, 3];
3.3 元组 Tuple
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
let x: [string, number];
x = ['hello', 10];
x = [10, 'hello'];
console.log(x[0].substr(1));
console.log(x[1].substr(1));
x[3] = 'world';
越界元素:当访问超出元组长度的元素时,它的类型会被限制为元祖中每个类型的联合类型
//测试时,IDE和编译都会报错,和文档不一致。
let arr:[string, number] = ['name', 20];
arr[0] = 'age';
arr[2] = 'string// 编译报错
arr[3] = 40;// 编译报错
arr[4] = true; // 编译报错
Type '"string"' is not assignable to type 'undefined'.
arr[2] = 'string';
Tuple type '[string, number]' of length '2' has no element at index '2'.
arr[2] = 'string';
3.4 枚举
enum
类型是对JavaScript标准数据类型的一个补充。
enum Color {Red, Green, Blue}
let c: Color = Color.Green
console.log(c)
let b:string=Color.Red
let a:number=Color.Blue
// 默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。
// 手动赋值时, 未赋值的枚举成员会接着上一个枚举项递增(初始化)
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;
// 全部都采用手动赋值
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;
//值是number|string
enum Color {Red = '1', Green = true, Blue = 1}
//通过值取名字(*)
enum Color {Red = '1' Blue = 1}
let c:string=Color[2]
let d:string=Color[1]
let e:string=Color['1']
3.5 Any
不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;
与object类比,Any直接跳过类型检查,object允许赋值,但还是存在类型检查
let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)
let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
let list: any[] = [1, true, "free"];
list[1] = 100;
3.6 void
void
类型像是与any
类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void
function warnUser(): void {
console.log("This is my warning message");
}
void
类型的变量没有什么大用,因为你只能为它赋予undefined
和null
let unusable: void = undefined;
3.7 null、undefined
let u: undefined = undefined;
let n: null = null;
默认情况下null
和undefined
是所有类型的子类型。 就是说你可以把 null
和undefined
赋值给number
类型的变量。指定了--strictNullChecks
标记,null
和undefined
只能赋值给void
和它们各自
tsc [filename] --strictNullChecks
3.8 Never
never
类型表示的是那些永不存在的值的类型。 例如, never
类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never
类型,当它们被永不为真的类型保护所约束时。
never
类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never
的子类型或可以赋值给never
类型(除了never
本身之外)。 即使 any
也不可以赋值给never
。
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}
// 推断的返回值类型为never
function fail() {
return error("Something failed");
}
// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {
}
}
3.9 Object
object
表示非原始类型,也就是除number
,string
,boolean
,symbol
,null
或undefined
之外的类型。
declare function create(o: object | null): void;
create({ prop: 0 }); // OK
create(null); // OK
create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error
declare
declare 定义的类型只会用于编译时的检查,编译结果中会被删除。
https://www.runoob.com/typescript/ts-ambient.html
3.10 类型断言
有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。
//方式一
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
//方式二
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
4.变量声明
let、const、var、解构、展开(…),与js一致
5.接口
可以理解为一种规范或者约束,用来描述 对象(object) 的形状 或者对 类(class) 的行为 进行抽象。
4.1 接口定义
使用 interface 定义接口, 接口名称一般首字母大写,定义接口的时候,只定义声明即可,不包含具体内容;
实现接口的时候,要实现里面的内容,定义的变量比接口少了或多了属性都是不允许的;
类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以;
// 定义一个接口 Person
interface Person {
name: string;
age: number;
}
// 定义一个个变量,它的类型是 Person
let tom: Person = {
name: 'Tom',
age: 25
};
let tom: Person = {
name: 'tom'
}// 编译报错,少了age属性
let tom: Person = {
name: 'Tom',
age: 12,
sex:"男"
}// 编译报错,多了sex属性
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
// let myObj = {size: 10, label: "Size 10 Object"};
// printLabel({size: 10, label: "Size 10 Object"});
interface LabelledValue {
label: string;
[propName: string]: any;// 添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
printLabel({size: 10, label: "Size 10 Object"});
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
printLabel({size: 10, label: "Size 10 Object"} as LabelledValue);
4.2 可选属性
使用 ? 代表可选属性, 即该属性可以不存在, 但不允许添加未定义的属性
interface Person {
name: string;
age?: number;
}
let tom: Person = {
name: 'tom'
}// age是可选属性
4.3 只读属性 readonly
对象属性只能在对象刚刚创建的时候修改其值。
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候
let person: Person = {
id: 100,
name: 'tom',
}
person05.id = 90;
// => 编译报错:id为只读, 不可修改
let person2: Person = {
name: 'welson',
age: 2
}
// => 编译报错:给对象 person2 赋值,未定义只读属性id
person2.id = 1;
// => 编译报错:id为只读, 不可修改
TypeScript具有ReadonlyArray
类型,它与Array
相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
4.4 额外的属性检查
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
// let myObj = {size: 10, label: "Size 10 Object"};
// printLabel(myObj);
// printLabel({size: 10, label: "Size 10 Object"});
interface LabelledValue {
label: string;
[propName: string]: any;// 添加一个字符串索引签名,表示LabelledValue可以有任意数量的属性。
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
printLabel({size: 10, label: "Size 10 Object"});
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
printLabel({size: 10, label: "Size 10 Object"} as LabelledValue);
propName: string]: string;
定义了任意属性,那么确定属性和可选属性都必须是它的子属性
interface Person {
name: string;
age?: number;
[propName: string]: string;
}
// 编译报错:Person定义了一个任意属性,其值为string类型。则Person的所有属性都必须为string类型,而age为number类型
4.5 函数类型
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。 比如,我们使用下面的代码重写上面的例子:
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
4.6 可索引的类型
可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number
来索引时,JavaScript会将它转换成string
然后再去索引对象。 也就是说用 100
(一个number
)去索引等同于使用"100"
(一个string
)去索引,因此两者需要保持一致。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
interface NotOkay {
[x: number]: Dog;
[x: string]: Animal;
}
字符串索引声明了 obj.property
和obj["property"]
两种形式都可以。 下面的例子里, name
的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型与索引类型返回值的类型不匹配
}
可以将索引签名设置为只读,这样就防止了给索引赋值:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
4.7 类类型
定义
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
举例: 人是一个类,人需要吃东西。动物是一个类,动物也需要吃东西。这种情况就可以把 吃东西 提取出来作为一个接口:
interface Ieat {
eat();
}
class Person implements Ieat{
eat(){}
}
class Animal implements Ieat {
eat(){}
}
一个类也可以实现多个接口:
interface Ieat {
eat();
}
interface Isleep {
sleep();
}
class Person implements Ieat, Isleep{
eat(){}
sleep() {}
}
接口继承接口
interface Alarm {
alert();
}
interface LightableAlarm extends Alarm {
lightOn();
lightOff();
}
接口继承类
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
混合类型
前面介绍了接口可以用来定义函数的形状,有时候,一个函数还可以有自己的属性和方法:
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
6.类
同ES6 的 class
相关概念
- 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
- 对象(Object):类的实例,通过
new
生成 - 面向对象(OOP)的三大特性:封装、继承、多态
- 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
- 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
- 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如
Cat
和Dog
都继承自Animal
,但是分别实现了自己的eat
方法。此时针对某一个实例,我们无需了解它是Cat
还是Dog
,就可以直接调用eat
方法,程序会自动判断出来应该如何执行eat
- 存取器(getter & setter):用以改变属性的读取和赋值行为
- 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如
public
表示公有属性或方法 - 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
- 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
6.1 类的定义
使用 class
定义类,使用 constructor
定义构造函数。
通过 new
生成新实例的时候,会自动调用构造函数。
class Animal {
name:string; // 定义属性
constructor(name) {
this.name = name; // 属性赋值
}
sayHi() {
return `我叫 ${this.name}`;
}
}
let cat = new Animal('Tom');
console.log(cat.sayHi()); // 我叫 Tom
6.2 类的继承
使用 extends
关键字实现继承,子类中使用 super
关键字来调用父类的构造函数和方法。
class Cat extends Animal {
color: string;
constructor(name, color) {
super(name); // 调用父类Animal的 constructor(name)
this.color = color
}
sayHi() {
// 调用父类的 sayHi();
return super.sayHi() + '我是一只'+ this.color + ' 色的猫,';
}
}
let c = new Cat('Tom', '橘黄'); // Tom
console.log(c.sayHi()); // 我叫 Tom,我是一只橘黄色的猫;
let cat2 = new Cat('Jerry');
cat2.color = '黑';
console.log(c.sayHi()); // 我叫 Jerry,我是一只黑色的猫;
6.3 存取器
使用 getter 和 setter 可以改变属性的赋值和读取行为:
class Animal {
name:string;
constructor(name) {
this.name = name;
}
get name():string {
return 'Jack';
}
set name(value) {
console.log('setter: ' + value);
}
}
let a = new Animal('Kitty'); // setter: Kitty
a.name = 'Tom'; // setter: Tom
console.log(a.name); // Jack
6.4 实例属性和方法
js中的属性和方法:
// js中
function Person(name) {
this.name = name; // 实例属性
this.eat = function(){ console.log('eat') }; // 实例方法
}
Person.age = 19; // 静态属性
Person.sleep = function(){ console.log('sleep') }; // 静态方法
// 访问实例方法和属性:
var tom = new Person('tom');
console.log(tom.name) // tom
tom.eat();
tom.sleep() // error: tom.sleep is not a function
// 访问静态方法和属性:
console.log(Person.age); // 19
Person.sleep();
Person.eat(); // error: Person.eat is not a function
// ES6
class Animal {
name:'',
constructor(){
this.name = 'tom';
}
eat() {}
}
let a = new Animal();
console.log(a.name); // tom
ts:
// ts
class Animal {
name:string = 'tom';
eat() {}
}
let a = new Animal();
console.log(a.name); // Jack
6.5 静态属性和方法
ES7 提案中,可以使用 static
定义一个静态属性或方法。静态方法不需要实例化,而是直接通过类来调用:
// ts
class Animal {
static num = 42;
static isAnimal(a) {
return a instanceof Animal;
}
}
console.log(Animal.num); // 42
let a = new Animal('Jack');
Animal.isAnimal(a); // true
a.isAnimal(a); // TypeError: a.isAnimal is not a function
不会被继承,只能用构造函数.
访问,不能用this.
;类具有 实例部分与 静态部分这两个部分.
6.6 访问修饰符
class Person {
public name:string;
private idCard:number;
protected phone:number;
constructor(name,idCard,phone) {
this.name = name;
this.idCard = idCard;
this.phone = phone;
}
}
let tom = new Person('tom',420000,13811110000);
console.log(tom.name) // tom
console.log(tom.idCard)
// error:Property 'idCard' is private and only accessible within class 'Person'.
console.log(tom.phone)
// error:Property 'phone' is protected and only accessible within class 'Person' and its subclasses
class Teacher extends Person {
constructor(name,idCard,phone) {
super(name,idCard,phone);
console.log(this.name)
console.log(this.phone)
console.log(this.idCard)
// error:Property 'idCard' is private and only accessible within class 'Person'.
}
}
public
公有属性或方法,可以在任何地方被访问到,默认所有的属性和方法都是 public
的
private
私有属性或方法,不能在声明它的类的外部访问,也不可以在子类中访问
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() {
super(name)
console.log(this.name) }
}
protected
受保护的属性或方法,它和 private
类似,区别是它可以在子类中访问
readonly修饰符
你可以使用 readonly
关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.
6.7 多态
同一个父类的多个子类,可以有不同结果的同名方法:
class Person {
eat(){ console.log('eat') }
}
class A extends Person {
eat(){ console.log('A eat') }
}
class B extends Person {
eat(){ console.log('B eat') }
}
6.8 抽象类/抽象方法 abstract
abstract
用于定义抽象类和其中的抽象方法。
- 抽象类是提供给其他类继承的基类(父类),是不允许被实例化
- 抽象方法只能包含在抽象类中
- 子类继承抽象类,必须实现抽象类中的抽象方法
abstract class Animal {
abstract eat(); // 抽象方法
// 普通方法
sleep(){
console.log('sleep')
}
}
let a = new Animal(); // 报错,抽象类不能被实例化
class Cat extends Animal {
eat(){
// 父类的eat方法必须被实现
console.log('eat')
}
}
7.函数
7.1 函数声明
function sum(x: number, y: number): number {
return x + y;
}
输入多余的(或者少于要求的)参数,是不被允许的
sum(1, 2, 3);
// 编译报错:多了1个参数
7.2 匿名函数(函数表达式)
let mySum = function (x: number, y: number): number {
return x + y;
};
上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum
,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 mySum
添加类型,则应该是这样:
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
// 注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>
7.3 用接口定义函数的形状
interface FuncAdd {
(value: number, increment: number): number
}
let add: FuncAdd;
add = function(value: number, increment: number): number {
return value + increment;
}
// 函数的参数名不需要与接口里定义的名字相匹配
let add2: FuncAdd;
add2 = function(a: number, b: number) {
return a + b;
}
let add2: FuncAdd;
add2 = function(a, b) {
return a + b;
}
7.4 可选参数
可选参数必须接在必需参数后面,换句话说,可选参数后面不允许再出现必须参数了
function addNum(a: number, b: number, c? :number): number {
if(c) {
return a + b + c;
} else {
return a + b;
}
}
console.log(add(1, 2));
7.5 默认参数
类比 ES6 中的默认值
function add(a: number = 1, b: number): number {
return a + b;
}
console.log(add(undefined, 1));
7.6 剩余参数
类比 Es6 中对象展开
interface AddFunc {
(num1: number, ...rest: number[]): number
}
let add: AddFunc;
add = function(a: number, ...rest: number[]): number {
let result = a;
rest.map(v => result += v);
return result;
}
console.log(add(1,2,3,4));
7.7 函数重载
重载是为同一个函数提供多个函数类型定义,允许函数对传入不同的参数返回不同的的结果分别做类型检查
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x): any {
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
let pickedCard2 = pickCard(15);
缺点:x可以是任意类型
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: {suit: string; card: number; }[]): number; //定义
function pickCard(x: number): {suit: string; card: number; }; //定义
function pickCard(x): any { //实现
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
let pickedCard2 = pickCard(15);
这样改变后,重载的pickCard
函数在调用的时候会进行正确的类型检查。
为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。
重复定义多次函数 pickCard
,前几次都是函数定义,最后一次是函数实现。
注意,function pickCard(x): any
并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用 pickCard
会产生错误。
8.泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
function identity(arg: any): any {
return arg;
}
使用any
类型会导致这个函数可以接收任何类型的arg
参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。
8.1 泛型函数
我们给identity添加了类型变量T
。 T
帮助我们捕获用户传入的类型(比如:number
),之后我们就可以使用这个类型。 之后我们再次使用了 T
当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。
function identity<T>(arg: T): T {
return arg;
}
// 使用1
let output = identity<string>("myString");
// 使用2 第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型
let output = identity("myString");
也可以同时使用多个类型参数:
function swap<S,P>(tuple:[S,P]):[P,S] {
return [tuple[1], tuple[0]]
}
swap<string, number>(['a', 2])
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
loggingIdentity([1,2]) //ok
loggingIdentity(1) //err
loggingIdentity({}) //err
泛型函数loggingIdentity
,接收类型参数T
和参数arg
,它是个元素类型是T
的数组,并返回元素类型是T
的数组。
泛型函数定义:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
// 可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以
let myIdentity2: <U>(arg: U) => U = identity;
// 还可以使用带有调用签名的对象字面量来定义泛型函数
let myIdentity3: {<T>(arg: T): T} = identity;
8.2 泛型类
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
8.3 泛型接口
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
8.4 泛型约束
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法
获取一个参数的长度:
function getLength<T>(arg:T):T {
console.log(arg.length) // error: Property 'length' does not exist on type 'T'
return arg;
}
上例中,泛型 T
不一定包含属性 length
,所以编译的时候报错了,这时候就可以使用泛型约束,使用 extends
约束泛型 T
必须符合 Ilength
的形状,也就是必须包含 length
属性:
interface Ilength {
length: number
}
function getLength<T extends Ilength>(arg:T):T {
console.log(arg.length)
return arg;
}
getLength('abcd') // 4
getLength(7) // error: Argument of type '7' is not assignable to parameter of type 'Ilength'.
多个参数间也可以互相约束:
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id];
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 })
上例中,使用了两个类型参数,其中要求 T
继承 U
,这样就保证了 U
上不会出现 T
中不存在的字段。
function getProperty<T, K extends keyof T>(obj: T, key: K):T[K] {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
9. 枚举
枚举类型用于取值被限定在一定范围内的场景,如一周只有7天,一年只有4季等。
9.1 枚举初始化
枚举初始化可以理解为给枚举成员赋值。每个枚举成员都需要带有一个值,在未赋值的情况下, 枚举成员会被赋值为从 0
开始, 步长为 1 递增的数字:
enum Weeks {Mon, Tue, Wed, Thu, Fri, Sat, Sun};
console.log(Weeks['Mon']); // 0
console.log(Weeks[0]); // 'Mon'
console.log(Weeks.Tue); // 1
手动赋值时, 未赋值的枚举成员会接着上一个枚举项递增(初始化):
enum Weeks {
Mon, Tue, Wed, Thu = 2, Fri, Sat = -1.5, Sun
};
console.log(Weeks['Mon']); // => 0
console.log(Weeks.Wed); // => 2
console.log(Weeks.Thu); // => 2
console.log(Weeks.Fri); // => 3
console.log(Weeks.Sun); // => -0.5
上例中,未手动赋值的 Wed 和手动赋值的 Thu 取值重复了,但是 TypeScript 并不会报错,该种情况可能会引起取值错误,所以使用的时候最好避免出现取值重复的情况。
TypeScript 支持数字的和基于字符串的枚举。
9.2 数字枚举
enum Weeks {
Sun, Mon, Tue, Wed, Thu, Fri, Sat
};
9.3 字符串枚举
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
9.4 异构枚举
可以混合字符串和数字,但通常不这么做
enum Gender {
Male = 0,
Female = "1",
}
9.5 常量成员和计算所得成员
枚举成员的值可以是 常量 或 计算出来的。
上面所举的例子都是常量成员,官网定义如下:
当满足以下条件时,枚举成员被当作是常数:
- 它是枚举的第一个成员且没有初始化函数,这种情况下它被赋予值
0
// E.X is constant:
enum E { X }
- 不具有初始化函数并且之前的枚举成员是数字常量。在这种情况下,当前枚举成员的值为上一个枚举成员的值加
1
。但第一个枚举元素是个例外。如果它没有初始化方法,那么它的初始值为0
。
// All enum members in 'E1' and 'E2' are constant.
enum E1 { X, Y, Z }
enum E2 {
A = 1, B, C
}
- 枚举成员使用常数枚举表达式初始化。常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。当一个表达式满足下面条件之一时,它就是一个常数枚举表达式:
- 数字字面量
- 引用之前定义的常数枚举成员(可以是在不同的枚举类型中定义的)如果这个成员是在同一个枚举类型中定义的,可以使用非限定名来引用
- 带括号的常数枚举表达式
-
+
,-
,~
一元运算符应用于常数枚举表达式 -
+
,-
,*
,/
,%
,<<
,>>
,>>>
,&
,|
,^
二元运算符,常数枚举表达式做为其一个操作对象。若常数枚举表达式求值后为 NaN 或 Infinity,则会在编译阶段报错
所有其它情况的枚举成员被当作是需要计算得出的值。
enum FileAccess {
// constant members
None, //0
Read = 1 << 1, //2
Write = 1 << 2, //4
ReadWrite = Read | Write, //6
// computed member
G = "123".length //不在编译阶段求值
}
9.6 反向映射
除了创建一个以属性名做为对象成员的对象之外,数字枚举成员还具有了 反向映射,从枚举值到枚举名字。 例如,在下面的例子中:
enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[a];
TypeScript可能会将这段代码编译为下面的JavaScript:
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A; //0
var nameOfA = Enum[a]; // "A"
生成的代码中,枚举类型被编译成一个对象,它包含了正向映射( name
-> value
)和反向映射( value
-> name
)。 引用枚举成员总会生成为对属性访问并且永远也不会内联代码。
要注意的是不会为字符串枚举成员生成反向映射:
enum Enum {
A="a"
}
let a = Enum.A;
let nameOfA = Enum[a];
编译后:
var Enum;
(function (Enum) {
Enum["A"] = "a";
})(Enum || (Enum = {}));
var a = Enum.A; //a
var nameOfA = Enum[a]; // undefined
9.7 常量枚举 const enum
常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员。
const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions['Down'], Directions.Left, Directions.Right]
编译后:
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
不是常量枚举:
enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions['Down'], Directions.Left, Directions.Right]
编译后:
var Directions;
(function (Directions) {
Directions[Directions["Up"] = 0] = "Up";
Directions[Directions["Down"] = 1] = "Down";
Directions[Directions["Left"] = 2] = "Left";
Directions[Directions["Right"] = 3] = "Right";
})(Directions || (Directions = {}));
var directions = [Directions.Up, Directions['Down'], Directions.Left, Directions.Right];
9.8 外部枚举 declare enum
外部枚举与声明语句一样,常出现在声明文件中。
declare enum Directions {
Up,
Down,
Left,
Right
} //编译时删除
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
编译后:
var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
同时使用 declare
和 const
也是可以的,编译结果同常量枚举一致。
10. 类型推论
10.1 基础
变量声明如果没有明确的指定类型,那么 TypeScript 会依照类型推论的规则推断出一个类型
let a = 1 // 推断为 number
let b = [1] // 推断为 number[]
let c = (x = 1) => x + 1 // 推断为 (x?: number) => number
let string = 'seven';
// 等价于 let string: string = 'seven';
string = 4;
// 编译报错: error TS2322: Type 'number' is not assignable to type 'string'
变量声明但是未赋值,会推论为 any
let x;
x = 1;
x = 'aaa'
10.2 最佳通用类型推断
当需要从多个类型中推断出一个类型的时候,TypeScript 会尽可能的推断出一个兼容当前所有类型的通用类型。
let d = [1, null]
// 推断为一个最兼容的类型,所以推断为(number | null)[]
// 当关闭"strictNullChecks"配置项时,null是number的子类型,所以推断为number[]
10.3 上下文类型推断
以上的推断都是从右向左,即根据表达式推断,上下文类型推断是从左向右。
TypeScript类型推论也可能按照相反的方向进行,即‘按上下文归类’。表达式的类型与所处的位置相关。
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.buton); //<- Error
}; //实际不报错
使用any类型,可以使编译器忽略,类型判断
window.onmousedown = function(mouseEvent: any) {
console.log(mouseEvent.buton); //<- Now, no error is given
}
11 类型兼容性
当一个类型Y可以被赋值给另一个类型X时,我们就可以说类型X兼容类型Y。
X兼容Y:X(目标类型) = Y(源类型)
let s: string = 'a'
s = null
// 把编译配置中的strictNullChecks设置成false,字符类型是兼容null类型的(因为null是字符的子类型)
11.1 接口兼容
成员少的兼容成员多的
interface X {
a: any
b: any
}
interface Y {
a: any
b: any
c: any
}
let x: X = { a: 1, b: 2 }
let y: Y = { a: 1, b: 2, c: 3 }
// 源类型只要具有目标类型的必要属性,就可以进行赋值。接口之间相互兼容,成员少的兼容成员多的。
x = y
// y = x // 不兼容
11.2 函数兼容性
type Handler = (a: number, b: number) => void
function test(handler: Handler) {
return handler
}
1、参数个数
固定参数
目标函数的参数个数一定要多于源函数的参数个数。
Handler 目标函数,传入 test 的 参数函数 就是源函数。
let handler1 = (a: number) => {}
test(handler1) // 传入的函数能接收一个参数,且参数是number,是兼容的
let handler2 = (a: number, b: number, c: number) => {}
test(handler2) // 会报错 传入的函数能接收三个参数(参数多了),且参数是number,是不兼容的
可选参数和剩余参数
let a1 = (p1: number, p2: number) => { }
let b1 = (p1?: number, p2?: number) => { }
let c1 = (...args: number[]) => { }
(1) 固定参数是可以兼容可选参数和剩余参数的。
a1 = b1 // 兼容
a1 = c1 // 兼容
(2) 可选参数是不兼容固定参数和剩余参数的,但是可以通过设置"strictFunctionTypes": false来消除报错,实现兼容。
b1 = a1 //不兼容
b1 = c1 // 不兼容
(3) 剩余参数可以兼容固定参数和可选参数。
c1 = a1 // 兼容
c1 = b1 // 兼容
2、参数类型
基础类型
// 接上面的test函数
let handler3 = (a: string) => { }
test(handler3) // 类型不兼容
接口类型
接口成员多的兼容成员少的,也可以理解把接口展开,参数多的兼容参数少的。对于不兼容的,也可以通过设置"strictFunctionTypes": false来消除报错,实现兼容。
interface Point3D {
x: number
y: number
z: number
}
interface Point2D {
x: number
y: number
}
let p3d = (point: Point3D) => {}
let p2d = (point: Point2D) => {}
p3d = p2d // 兼容
p2d = p3d // 不兼容
3、返回值类型 目标函数的返回值类型必须与源函数的返回值类型相同,或者是其子类型。
let f = () => ({ name: 'Alice' })
let g = () => ({ name: 'A', location: 'beijing' })
f = g // 兼容
g = f // 不兼容
4、函数重载 函数重载列表(目标函数)
function overload(a: number, b: number): number;
function overload(a: string, b: string): string;
函数的具体实现(源函数)
function overload(a: any, b: any): any { }
目标函数的参数要多于源函数的参数才能兼容
function overload(a:any,b:any,c:any):any {}
// 不兼容,具体实现时的参数多于重载列表中匹配到的第一个定义的函数的参数,也就是源函数的参数多于目标函数的参数
返回值类型不兼容
function overload(a:any,b:any) {}
// 去掉了返回值的any,不兼容
11.3 枚举类型兼容性
enum Fruit { Apple, Banana }
enum Color { Red, Yello }
枚举类型和数字类型是完全兼容的
let fruit: Fruit.Apple = 4
let no: number = Fruit.Apple
枚举类型之间是完全不兼容的
let color: Color.Red = Fruit.Apple // 不兼容
11.4 类的兼容性
和接口比较相似,只比较结构,需要注意,在比较两个类是否兼容时,静态成员和构造函数是不参与比较的,如果两个类具有相同的实例成员,那么他们的实例就相互兼容。
class A {
constructor(p: number, q: number) {}
id: number = 1
}
class B {
static s = 1
constructor(p: number) {}
id: number = 2
}
let aa = new A(1, 2)
let bb = new B(1)
// 两个实例完全兼容,静态成员和构造函数是不比较的
aa = bb
bb = aa
私有属性
类中存在私有属性情况有两种,如果其中一个类有私有属性,另一个没有。没有的可以兼容有的,如果两个类都有,那两个类都不兼容。
如果一个类中有私有属性,另一个类继承了这个类,那么这两个类就是兼容的。
class A {
constructor(p: number, q: number) {}
id: number = 1
private name: string = '' // 只在A类中加这个私有属性,aa不兼容bb,但是bb兼容aa,如果A、B两个类中都加了私有属性,那么都不兼容
}
class B {
static s = 1
constructor(p: number) {}
id: number = 2
}
let aa = new A(1, 2)
let bb = new B(1)
aa = bb // 不兼容
bb = aa // 兼容
// A中有私有属性,C继承A后,aa和cc是相互兼容的
class C extends A {}
let cc = new C(1, 2)
// 两个类的实例是兼容的
aa = cc
cc = aa
11.5 泛型兼容
泛型接口 泛型接口为空时,泛型指定不同的类型,也是兼容的。
interface Empty<T> {}
let obj1:Empty<number> = {}
let obj2:Empty<string> = {}
// 兼容
obj1 = obj2
obj2 = obj1
如果泛型接口中有一个接口成员时,类型不同就不兼容了。
interface Empty<T> {
value: T
}
let obj1:Empty<number> = {}
let obj2:Empty<string> = {}
// 报错,都不兼容
obj1 = obj2
obj2 = obj1
泛型函数 两个泛型函数如果定义相同,没有指定类型参数的话也是相互兼容的。
let log1 = <T>(x: T): T => {
return x
}
let log2 = <U>(y: U): U => {
return y
}
log1 = log2
log2 = log1
11.6 兼容性总结
- 结构之间兼容:成员少的兼容成员多的;
- 函数之间兼容:参数多的兼容参数少的。
TypeScript
它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程,支持ES6。
- 编译型语言:编译为 js 后运行,单独无法运行;
- 强类型语言;
- 面向对象的语言;
优势
- 类型系统实际上是最好的文档,大部分的函数看看类型的定义就可以知道如何使用;
- 可以在编译阶段就发现大部分错误,这总比在运行时候出错好;
- 增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等;
总结:TypeSctipt增加了代码的可读性和可维护性。
1.安装
npm install -g typescript
tsc -v
2.编译代码
tsc xxx.ts
生成xxx.js
3.数据类型
3.1 boolean number string
let name: type
3.2 Array
let list: number[] = [1, 2, 3];
//或
let list: Array<number> = [1, 2, 3];
3.3 元组 Tuple
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
let x: [string, number];
x = ['hello', 10];
x = [10, 'hello'];
console.log(x[0].substr(1));
console.log(x[1].substr(1));
x[3] = 'world';
越界元素:当访问超出元组长度的元素时,它的类型会被限制为元祖中每个类型的联合类型
//测试时,IDE和编译都会报错,和文档不一致。
let arr:[string, number] = ['name', 20];
arr[0] = 'age';
arr[2] = 'string// 编译报错
arr[3] = 40;// 编译报错
arr[4] = true; // 编译报错
Type '"string"' is not assignable to type 'undefined'.
arr[2] = 'string';
Tuple type '[string, number]' of length '2' has no element at index '2'.
arr[2] = 'string';
3.4 枚举
enum
类型是对JavaScript标准数据类型的一个补充。
enum Color {Red, Green, Blue}
let c: Color = Color.Green
console.log(c)
let b:string=Color.Red
let a:number=Color.Blue
// 默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。
// 手动赋值时, 未赋值的枚举成员会接着上一个枚举项递增(初始化)
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;
// 全部都采用手动赋值
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;
//值是number|string
enum Color {Red = '1', Green = true, Blue = 1}
//通过值取名字(*)
enum Color {Red = '1' Blue = 1}
let c:string=Color[2]
let d:string=Color[1]
let e:string=Color['1']
3.5 Any
不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;
与object类比,Any直接跳过类型检查,object允许赋值,但还是存在类型检查
let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)
let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
let list: any[] = [1, true, "free"];
list[1] = 100;
3.6 void
void
类型像是与any
类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void
function warnUser(): void {
console.log("This is my warning message");
}
void
类型的变量没有什么大用,因为你只能为它赋予undefined
和null
let unusable: void = undefined;
3.7 null、undefined
let u: undefined = undefined;
let n: null = null;
默认情况下null
和undefined
是所有类型的子类型。 就是说你可以把 null
和undefined
赋值给number
类型的变量。指定了--strictNullChecks
标记,null
和undefined
只能赋值给void
和它们各自
tsc [filename] --strictNullChecks
3.8 Never
never
类型表示的是那些永不存在的值的类型。 例如, never
类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never
类型,当它们被永不为真的类型保护所约束时。
never
类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never
的子类型或可以赋值给never
类型(除了never
本身之外)。 即使 any
也不可以赋值给never
。
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}
// 推断的返回值类型为never
function fail() {
return error("Something failed");
}
// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {
}
}
3.9 Object
object
表示非原始类型,也就是除number
,string
,boolean
,symbol
,null
或undefined
之外的类型。
declare function create(o: object | null): void;
create({ prop: 0 }); // OK
create(null); // OK
create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error
declare
declare 定义的类型只会用于编译时的检查,编译结果中会被删除。
https://www.runoob.com/typescript/ts-ambient.html
3.10 类型断言
有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。
//方式一
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
//方式二
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
4.变量声明
let、const、var、解构、展开(…),与js一致
5.接口
可以理解为一种规范或者约束,用来描述 对象(object) 的形状 或者对 类(class) 的行为 进行抽象。
4.1 接口定义
使用 interface 定义接口, 接口名称一般首字母大写,定义接口的时候,只定义声明即可,不包含具体内容;
实现接口的时候,要实现里面的内容,定义的变量比接口少了或多了属性都是不允许的;
类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以;
// 定义一个接口 Person
interface Person {
name: string;
age: number;
}
// 定义一个个变量,它的类型是 Person
let tom: Person = {
name: 'Tom',
age: 25
};
let tom: Person = {
name: 'tom'
}// 编译报错,少了age属性
let tom: Person = {
name: 'Tom',
age: 12,
sex:"男"
}// 编译报错,多了sex属性
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
// let myObj = {size: 10, label: "Size 10 Object"};
// printLabel({size: 10, label: "Size 10 Object"});
interface LabelledValue {
label: string;
[propName: string]: any;// 添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
printLabel({size: 10, label: "Size 10 Object"});
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
printLabel({size: 10, label: "Size 10 Object"} as LabelledValue);
4.2 可选属性
使用 ? 代表可选属性, 即该属性可以不存在, 但不允许添加未定义的属性
interface Person {
name: string;
age?: number;
}
let tom: Person = {
name: 'tom'
}// age是可选属性
4.3 只读属性 readonly
对象属性只能在对象刚刚创建的时候修改其值。
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候
let person: Person = {
id: 100,
name: 'tom',
}
person05.id = 90;
// => 编译报错:id为只读, 不可修改
let person2: Person = {
name: 'welson',
age: 2
}
// => 编译报错:给对象 person2 赋值,未定义只读属性id
person2.id = 1;
// => 编译报错:id为只读, 不可修改
TypeScript具有ReadonlyArray
类型,它与Array
相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
4.4 额外的属性检查
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
// let myObj = {size: 10, label: "Size 10 Object"};
// printLabel(myObj);
// printLabel({size: 10, label: "Size 10 Object"});
interface LabelledValue {
label: string;
[propName: string]: any;// 添加一个字符串索引签名,表示LabelledValue可以有任意数量的属性。
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
printLabel({size: 10, label: "Size 10 Object"});
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
printLabel({size: 10, label: "Size 10 Object"} as LabelledValue);
propName: string]: string;
定义了任意属性,那么确定属性和可选属性都必须是它的子属性
interface Person {
name: string;
age?: number;
[propName: string]: string;
}
// 编译报错:Person定义了一个任意属性,其值为string类型。则Person的所有属性都必须为string类型,而age为number类型
4.5 函数类型
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。 比如,我们使用下面的代码重写上面的例子:
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
4.6 可索引的类型
可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number
来索引时,JavaScript会将它转换成string
然后再去索引对象。 也就是说用 100
(一个number
)去索引等同于使用"100"
(一个string
)去索引,因此两者需要保持一致。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
interface NotOkay {
[x: number]: Dog;
[x: string]: Animal;
}
字符串索引声明了 obj.property
和obj["property"]
两种形式都可以。 下面的例子里, name
的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型与索引类型返回值的类型不匹配
}
可以将索引签名设置为只读,这样就防止了给索引赋值:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
4.7 类类型
定义
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
举例: 人是一个类,人需要吃东西。动物是一个类,动物也需要吃东西。这种情况就可以把 吃东西 提取出来作为一个接口:
interface Ieat {
eat();
}
class Person implements Ieat{
eat(){}
}
class Animal implements Ieat {
eat(){}
}
一个类也可以实现多个接口:
interface Ieat {
eat();
}
interface Isleep {
sleep();
}
class Person implements Ieat, Isleep{
eat(){}
sleep() {}
}
接口继承接口
interface Alarm {
alert();
}
interface LightableAlarm extends Alarm {
lightOn();
lightOff();
}
接口继承类
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
混合类型
前面介绍了接口可以用来定义函数的形状,有时候,一个函数还可以有自己的属性和方法:
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
6.类
同ES6 的 class
相关概念
- 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
- 对象(Object):类的实例,通过
new
生成 - 面向对象(OOP)的三大特性:封装、继承、多态
- 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
- 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
- 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如
Cat
和Dog
都继承自Animal
,但是分别实现了自己的eat
方法。此时针对某一个实例,我们无需了解它是Cat
还是Dog
,就可以直接调用eat
方法,程序会自动判断出来应该如何执行eat
- 存取器(getter & setter):用以改变属性的读取和赋值行为
- 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如
public
表示公有属性或方法 - 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
- 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
6.1 类的定义
使用 class
定义类,使用 constructor
定义构造函数。
通过 new
生成新实例的时候,会自动调用构造函数。
class Animal {
name:string; // 定义属性
constructor(name) {
this.name = name; // 属性赋值
}
sayHi() {
return `我叫 ${this.name}`;
}
}
let cat = new Animal('Tom');
console.log(cat.sayHi()); // 我叫 Tom
6.2 类的继承
使用 extends
关键字实现继承,子类中使用 super
关键字来调用父类的构造函数和方法。
class Cat extends Animal {
color: string;
constructor(name, color) {
super(name); // 调用父类Animal的 constructor(name)
this.color = color
}
sayHi() {
// 调用父类的 sayHi();
return super.sayHi() + '我是一只'+ this.color + ' 色的猫,';
}
}
let c = new Cat('Tom', '橘黄'); // Tom
console.log(c.sayHi()); // 我叫 Tom,我是一只橘黄色的猫;
let cat2 = new Cat('Jerry');
cat2.color = '黑';
console.log(c.sayHi()); // 我叫 Jerry,我是一只黑色的猫;
6.3 存取器
使用 getter 和 setter 可以改变属性的赋值和读取行为:
class Animal {
name:string;
constructor(name) {
this.name = name;
}
get name():string {
return 'Jack';
}
set name(value) {
console.log('setter: ' + value);
}
}
let a = new Animal('Kitty'); // setter: Kitty
a.name = 'Tom'; // setter: Tom
console.log(a.name); // Jack
6.4 实例属性和方法
js中的属性和方法:
// js中
function Person(name) {
this.name = name; // 实例属性
this.eat = function(){ console.log('eat') }; // 实例方法
}
Person.age = 19; // 静态属性
Person.sleep = function(){ console.log('sleep') }; // 静态方法
// 访问实例方法和属性:
var tom = new Person('tom');
console.log(tom.name) // tom
tom.eat();
tom.sleep() // error: tom.sleep is not a function
// 访问静态方法和属性:
console.log(Person.age); // 19
Person.sleep();
Person.eat(); // error: Person.eat is not a function
// ES6
class Animal {
name:'',
constructor(){
this.name = 'tom';
}
eat() {}
}
let a = new Animal();
console.log(a.name); // tom
ts:
// ts
class Animal {
name:string = 'tom';
eat() {}
}
let a = new Animal();
console.log(a.name); // Jack
6.5 静态属性和方法
ES7 提案中,可以使用 static
定义一个静态属性或方法。静态方法不需要实例化,而是直接通过类来调用:
// ts
class Animal {
static num = 42;
static isAnimal(a) {
return a instanceof Animal;
}
}
console.log(Animal.num); // 42
let a = new Animal('Jack');
Animal.isAnimal(a); // true
a.isAnimal(a); // TypeError: a.isAnimal is not a function
不会被继承,只能用构造函数.
访问,不能用this.
;类具有 实例部分与 静态部分这两个部分.
6.6 访问修饰符
class Person {
public name:string;
private idCard:number;
protected phone:number;
constructor(name,idCard,phone) {
this.name = name;
this.idCard = idCard;
this.phone = phone;
}
}
let tom = new Person('tom',420000,13811110000);
console.log(tom.name) // tom
console.log(tom.idCard)
// error:Property 'idCard' is private and only accessible within class 'Person'.
console.log(tom.phone)
// error:Property 'phone' is protected and only accessible within class 'Person' and its subclasses
class Teacher extends Person {
constructor(name,idCard,phone) {
super(name,idCard,phone);
console.log(this.name)
console.log(this.phone)
console.log(this.idCard)
// error:Property 'idCard' is private and only accessible within class 'Person'.
}
}
public
公有属性或方法,可以在任何地方被访问到,默认所有的属性和方法都是 public
的
private
私有属性或方法,不能在声明它的类的外部访问,也不可以在子类中访问
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() {
super(name)
console.log(this.name) }
}
protected
受保护的属性或方法,它和 private
类似,区别是它可以在子类中访问
readonly修饰符
你可以使用 readonly
关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.
6.7 多态
同一个父类的多个子类,可以有不同结果的同名方法:
class Person {
eat(){ console.log('eat') }
}
class A extends Person {
eat(){ console.log('A eat') }
}
class B extends Person {
eat(){ console.log('B eat') }
}
6.8 抽象类/抽象方法 abstract
abstract
用于定义抽象类和其中的抽象方法。
- 抽象类是提供给其他类继承的基类(父类),是不允许被实例化
- 抽象方法只能包含在抽象类中
- 子类继承抽象类,必须实现抽象类中的抽象方法
abstract class Animal {
abstract eat(); // 抽象方法
// 普通方法
sleep(){
console.log('sleep')
}
}
let a = new Animal(); // 报错,抽象类不能被实例化
class Cat extends Animal {
eat(){
// 父类的eat方法必须被实现
console.log('eat')
}
}
7.函数
7.1 函数声明
function sum(x: number, y: number): number {
return x + y;
}
输入多余的(或者少于要求的)参数,是不被允许的
sum(1, 2, 3);
// 编译报错:多了1个参数
7.2 匿名函数(函数表达式)
let mySum = function (x: number, y: number): number {
return x + y;
};
上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum
,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 mySum
添加类型,则应该是这样:
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
// 注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>
7.3 用接口定义函数的形状
interface FuncAdd {
(value: number, increment: number): number
}
let add: FuncAdd;
add = function(value: number, increment: number): number {
return value + increment;
}
// 函数的参数名不需要与接口里定义的名字相匹配
let add2: FuncAdd;
add2 = function(a: number, b: number) {
return a + b;
}
let add2: FuncAdd;
add2 = function(a, b) {
return a + b;
}
7.4 可选参数
可选参数必须接在必需参数后面,换句话说,可选参数后面不允许再出现必须参数了
function addNum(a: number, b: number, c? :number): number {
if(c) {
return a + b + c;
} else {
return a + b;
}
}
console.log(add(1, 2));
7.5 默认参数
类比 ES6 中的默认值
function add(a: number = 1, b: number): number {
return a + b;
}
console.log(add(undefined, 1));
7.6 剩余参数
类比 Es6 中对象展开
interface AddFunc {
(num1: number, ...rest: number[]): number
}
let add: AddFunc;
add = function(a: number, ...rest: number[]): number {
let result = a;
rest.map(v => result += v);
return result;
}
console.log(add(1,2,3,4));
7.7 函数重载
重载是为同一个函数提供多个函数类型定义,允许函数对传入不同的参数返回不同的的结果分别做类型检查
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x): any {
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
let pickedCard2 = pickCard(15);
缺点:x可以是任意类型
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: {suit: string; card: number; }[]): number; //定义
function pickCard(x: number): {suit: string; card: number; }; //定义
function pickCard(x): any { //实现
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
let pickedCard2 = pickCard(15);
这样改变后,重载的pickCard
函数在调用的时候会进行正确的类型检查。
为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。
重复定义多次函数 pickCard
,前几次都是函数定义,最后一次是函数实现。
注意,function pickCard(x): any
并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用 pickCard
会产生错误。
8.泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
function identity(arg: any): any {
return arg;
}
使用any
类型会导致这个函数可以接收任何类型的arg
参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。
8.1 泛型函数
我们给identity添加了类型变量T
。 T
帮助我们捕获用户传入的类型(比如:number
),之后我们就可以使用这个类型。 之后我们再次使用了 T
当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。
function identity<T>(arg: T): T {
return arg;
}
// 使用1
let output = identity<string>("myString");
// 使用2 第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型
let output = identity("myString");
也可以同时使用多个类型参数:
function swap<S,P>(tuple:[S,P]):[P,S] {
return [tuple[1], tuple[0]]
}
swap<string, number>(['a', 2])
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
loggingIdentity([1,2]) //ok
loggingIdentity(1) //err
loggingIdentity({}) //err
泛型函数loggingIdentity
,接收类型参数T
和参数arg
,它是个元素类型是T
的数组,并返回元素类型是T
的数组。
泛型函数定义:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
// 可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以
let myIdentity2: <U>(arg: U) => U = identity;
// 还可以使用带有调用签名的对象字面量来定义泛型函数
let myIdentity3: {<T>(arg: T): T} = identity;
8.2 泛型类
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
8.3 泛型接口
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
8.4 泛型约束
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法
获取一个参数的长度:
function getLength<T>(arg:T):T {
console.log(arg.length) // error: Property 'length' does not exist on type 'T'
return arg;
}
上例中,泛型 T
不一定包含属性 length
,所以编译的时候报错了,这时候就可以使用泛型约束,使用 extends
约束泛型 T
必须符合 Ilength
的形状,也就是必须包含 length
属性:
interface Ilength {
length: number
}
function getLength<T extends Ilength>(arg:T):T {
console.log(arg.length)
return arg;
}
getLength('abcd') // 4
getLength(7) // error: Argument of type '7' is not assignable to parameter of type 'Ilength'.
多个参数间也可以互相约束:
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id];
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 })
上例中,使用了两个类型参数,其中要求 T
继承 U
,这样就保证了 U
上不会出现 T
中不存在的字段。
function getProperty<T, K extends keyof T>(obj: T, key: K):T[K] {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
9. 枚举
枚举类型用于取值被限定在一定范围内的场景,如一周只有7天,一年只有4季等。
9.1 枚举初始化
枚举初始化可以理解为给枚举成员赋值。每个枚举成员都需要带有一个值,在未赋值的情况下, 枚举成员会被赋值为从 0
开始, 步长为 1 递增的数字:
enum Weeks {Mon, Tue, Wed, Thu, Fri, Sat, Sun};
console.log(Weeks['Mon']); // 0
console.log(Weeks[0]); // 'Mon'
console.log(Weeks.Tue); // 1
手动赋值时, 未赋值的枚举成员会接着上一个枚举项递增(初始化):
enum Weeks {
Mon, Tue, Wed, Thu = 2, Fri, Sat = -1.5, Sun
};
console.log(Weeks['Mon']); // => 0
console.log(Weeks.Wed); // => 2
console.log(Weeks.Thu); // => 2
console.log(Weeks.Fri); // => 3
console.log(Weeks.Sun); // => -0.5
上例中,未手动赋值的 Wed 和手动赋值的 Thu 取值重复了,但是 TypeScript 并不会报错,该种情况可能会引起取值错误,所以使用的时候最好避免出现取值重复的情况。
TypeScript 支持数字的和基于字符串的枚举。
9.2 数字枚举
enum Weeks {
Sun, Mon, Tue, Wed, Thu, Fri, Sat
};
9.3 字符串枚举
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
9.4 异构枚举
可以混合字符串和数字,但通常不这么做
enum Gender {
Male = 0,
Female = "1",
}
9.5 常量成员和计算所得成员
枚举成员的值可以是 常量 或 计算出来的。
上面所举的例子都是常量成员,官网定义如下:
当满足以下条件时,枚举成员被当作是常数:
- 它是枚举的第一个成员且没有初始化函数,这种情况下它被赋予值
0
// E.X is constant:
enum E { X }
- 不具有初始化函数并且之前的枚举成员是数字常量。在这种情况下,当前枚举成员的值为上一个枚举成员的值加
1
。但第一个枚举元素是个例外。如果它没有初始化方法,那么它的初始值为0
。
// All enum members in 'E1' and 'E2' are constant.
enum E1 { X, Y, Z }
enum E2 {
A = 1, B, C
}
- 枚举成员使用常数枚举表达式初始化。常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。当一个表达式满足下面条件之一时,它就是一个常数枚举表达式:
- 数字字面量
- 引用之前定义的常数枚举成员(可以是在不同的枚举类型中定义的)如果这个成员是在同一个枚举类型中定义的,可以使用非限定名来引用
- 带括号的常数枚举表达式
-
+
,-
,~
一元运算符应用于常数枚举表达式 -
+
,-
,*
,/
,%
,<<
,>>
,>>>
,&
,|
,^
二元运算符,常数枚举表达式做为其一个操作对象。若常数枚举表达式求值后为 NaN 或 Infinity,则会在编译阶段报错
所有其它情况的枚举成员被当作是需要计算得出的值。
enum FileAccess {
// constant members
None, //0
Read = 1 << 1, //2
Write = 1 << 2, //4
ReadWrite = Read | Write, //6
// computed member
G = "123".length //不在编译阶段求值
}
9.6 反向映射
除了创建一个以属性名做为对象成员的对象之外,数字枚举成员还具有了 反向映射,从枚举值到枚举名字。 例如,在下面的例子中:
enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[a];
TypeScript可能会将这段代码编译为下面的JavaScript:
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A; //0
var nameOfA = Enum[a]; // "A"
生成的代码中,枚举类型被编译成一个对象,它包含了正向映射( name
-> value
)和反向映射( value
-> name
)。 引用枚举成员总会生成为对属性访问并且永远也不会内联代码。
要注意的是不会为字符串枚举成员生成反向映射:
enum Enum {
A="a"
}
let a = Enum.A;
let nameOfA = Enum[a];
编译后:
var Enum;
(function (Enum) {
Enum["A"] = "a";
})(Enum || (Enum = {}));
var a = Enum.A; //a
var nameOfA = Enum[a]; // undefined
9.7 常量枚举 const enum
常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员。
const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions['Down'], Directions.Left, Directions.Right]
编译后:
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
不是常量枚举:
enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions['Down'], Directions.Left, Directions.Right]
编译后:
var Directions;
(function (Directions) {
Directions[Directions["Up"] = 0] = "Up";
Directions[Directions["Down"] = 1] = "Down";
Directions[Directions["Left"] = 2] = "Left";
Directions[Directions["Right"] = 3] = "Right";
})(Directions || (Directions = {}));
var directions = [Directions.Up, Directions['Down'], Directions.Left, Directions.Right];
9.8 外部枚举 declare enum
外部枚举与声明语句一样,常出现在声明文件中。
declare enum Directions {
Up,
Down,
Left,
Right
} //编译时删除
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
编译后:
var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
同时使用 declare
和 const
也是可以的,编译结果同常量枚举一致。
10. 类型推论
10.1 基础
变量声明如果没有明确的指定类型,那么 TypeScript 会依照类型推论的规则推断出一个类型
let a = 1 // 推断为 number
let b = [1] // 推断为 number[]
let c = (x = 1) => x + 1 // 推断为 (x?: number) => number
let string = 'seven';
// 等价于 let string: string = 'seven';
string = 4;
// 编译报错: error TS2322: Type 'number' is not assignable to type 'string'
变量声明但是未赋值,会推论为 any
let x;
x = 1;
x = 'aaa'
10.2 最佳通用类型推断
当需要从多个类型中推断出一个类型的时候,TypeScript 会尽可能的推断出一个兼容当前所有类型的通用类型。
let d = [1, null]
// 推断为一个最兼容的类型,所以推断为(number | null)[]
// 当关闭"strictNullChecks"配置项时,null是number的子类型,所以推断为number[]
10.3 上下文类型推断
以上的推断都是从右向左,即根据表达式推断,上下文类型推断是从左向右。
TypeScript类型推论也可能按照相反的方向进行,即‘按上下文归类’。表达式的类型与所处的位置相关。
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.buton); //<- Error
}; //实际不报错
使用any类型,可以使编译器忽略,类型判断
window.onmousedown = function(mouseEvent: any) {
console.log(mouseEvent.buton); //<- Now, no error is given
}
11 类型兼容性
当一个类型Y可以被赋值给另一个类型X时,我们就可以说类型X兼容类型Y。
X兼容Y:X(目标类型) = Y(源类型)
let s: string = 'a'
s = null
// 把编译配置中的strictNullChecks设置成false,字符类型是兼容null类型的(因为null是字符的子类型)
11.1 接口兼容
成员少的兼容成员多的
interface X {
a: any
b: any
}
interface Y {
a: any
b: any
c: any
}
let x: X = { a: 1, b: 2 }
let y: Y = { a: 1, b: 2, c: 3 }
// 源类型只要具有目标类型的必要属性,就可以进行赋值。接口之间相互兼容,成员少的兼容成员多的。
x = y
// y = x // 不兼容
11.2 函数兼容性
type Handler = (a: number, b: number) => void
function test(handler: Handler) {
return handler
}
1、参数个数
固定参数
目标函数的参数个数一定要多于源函数的参数个数。
Handler 目标函数,传入 test 的 参数函数 就是源函数。
let handler1 = (a: number) => {}
test(handler1) // 传入的函数能接收一个参数,且参数是number,是兼容的
let handler2 = (a: number, b: number, c: number) => {}
test(handler2) // 会报错 传入的函数能接收三个参数(参数多了),且参数是number,是不兼容的
可选参数和剩余参数
let a1 = (p1: number, p2: number) => { }
let b1 = (p1?: number, p2?: number) => { }
let c1 = (...args: number[]) => { }
(1) 固定参数是可以兼容可选参数和剩余参数的。
a1 = b1 // 兼容
a1 = c1 // 兼容
(2) 可选参数是不兼容固定参数和剩余参数的,但是可以通过设置"strictFunctionTypes": false来消除报错,实现兼容。
b1 = a1 //不兼容
b1 = c1 // 不兼容
(3) 剩余参数可以兼容固定参数和可选参数。
c1 = a1 // 兼容
c1 = b1 // 兼容
2、参数类型
基础类型
// 接上面的test函数
let handler3 = (a: string) => { }
test(handler3) // 类型不兼容
接口类型
接口成员多的兼容成员少的,也可以理解把接口展开,参数多的兼容参数少的。对于不兼容的,也可以通过设置"strictFunctionTypes": false来消除报错,实现兼容。
interface Point3D {
x: number
y: number
z: number
}
interface Point2D {
x: number
y: number
}
let p3d = (point: Point3D) => {}
let p2d = (point: Point2D) => {}
p3d = p2d // 兼容
p2d = p3d // 不兼容
3、返回值类型 目标函数的返回值类型必须与源函数的返回值类型相同,或者是其子类型。
let f = () => ({ name: 'Alice' })
let g = () => ({ name: 'A', location: 'beijing' })
f = g // 兼容
g = f // 不兼容
4、函数重载 函数重载列表(目标函数)
function overload(a: number, b: number): number;
function overload(a: string, b: string): string;
函数的具体实现(源函数)
function overload(a: any, b: any): any { }
目标函数的参数要多于源函数的参数才能兼容
function overload(a:any,b:any,c:any):any {}
// 不兼容,具体实现时的参数多于重载列表中匹配到的第一个定义的函数的参数,也就是源函数的参数多于目标函数的参数
返回值类型不兼容
function overload(a:any,b:any) {}
// 去掉了返回值的any,不兼容
11.3 枚举类型兼容性
enum Fruit { Apple, Banana }
enum Color { Red, Yello }
枚举类型和数字类型是完全兼容的
let fruit: Fruit.Apple = 4
let no: number = Fruit.Apple
枚举类型之间是完全不兼容的
let color: Color.Red = Fruit.Apple // 不兼容
11.4 类的兼容性
和接口比较相似,只比较结构,需要注意,在比较两个类是否兼容时,静态成员和构造函数是不参与比较的,如果两个类具有相同的实例成员,那么他们的实例就相互兼容。
class A {
constructor(p: number, q: number) {}
id: number = 1
}
class B {
static s = 1
constructor(p: number) {}
id: number = 2
}
let aa = new A(1, 2)
let bb = new B(1)
// 两个实例完全兼容,静态成员和构造函数是不比较的
aa = bb
bb = aa
私有属性
类中存在私有属性情况有两种,如果其中一个类有私有属性,另一个没有。没有的可以兼容有的,如果两个类都有,那两个类都不兼容。
如果一个类中有私有属性,另一个类继承了这个类,那么这两个类就是兼容的。
class A {
constructor(p: number, q: number) {}
id: number = 1
private name: string = '' // 只在A类中加这个私有属性,aa不兼容bb,但是bb兼容aa,如果A、B两个类中都加了私有属性,那么都不兼容
}
class B {
static s = 1
constructor(p: number) {}
id: number = 2
}
let aa = new A(1, 2)
let bb = new B(1)
aa = bb // 不兼容
bb = aa // 兼容
// A中有私有属性,C继承A后,aa和cc是相互兼容的
class C extends A {}
let cc = new C(1, 2)
// 两个类的实例是兼容的
aa = cc
cc = aa
11.5 泛型兼容
泛型接口 泛型接口为空时,泛型指定不同的类型,也是兼容的。
interface Empty<T> {}
let obj1:Empty<number> = {}
let obj2:Empty<string> = {}
// 兼容
obj1 = obj2
obj2 = obj1
如果泛型接口中有一个接口成员时,类型不同就不兼容了。
interface Empty<T> {
value: T
}
let obj1:Empty<number> = {}
let obj2:Empty<string> = {}
// 报错,都不兼容
obj1 = obj2
obj2 = obj1
泛型函数 两个泛型函数如果定义相同,没有指定类型参数的话也是相互兼容的。
let log1 = <T>(x: T): T => {
return x
}
let log2 = <U>(y: U): U => {
return y
}
log1 = log2
log2 = log1
11.6 兼容性总结
- 结构之间兼容:成员少的兼容成员多的;
- 函数之间兼容:参数多的兼容参数少的。