TypeScript

初次使用

安装 ts

1
2
3
4
5
6
7
npm i -g typescript
//查看版本
tsc -v

//使用tsc编译文件,但是每次都要运行
//监听模式可以实时编译
tsc -w

基础类型

数组

数组有两种定义方法.

  1. 在元素类型后接 [] .表示由此类型元素组成的一个数组.
1
let list: number[] = [1, 2, 3];
  1. 使用数组泛型.
1
let list: Array<number> = [1, 2, 3];

元组 Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同.(可以理解为混合数组).

1
2
3
let x: [string, number];
x = ["hello", 10]; //正确
x = [10, "hello"]; //错误

当访问一个越界的元素,会使用联合类型替代:

1
2
3
x[3] = "world"; // OK, 字符串可以赋值给(string | number)类型
console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString
x[6] = true; // Error, 布尔不是(string | number)类型

在 react 中使用

便于重命名

1
2
3
4
5
const useHappy = () => {
return [isHappy,makeTomHappy,makeUnHappy]
}
const SomeComponent = () => {
const [tomIsHappy,makeTomHappy,makeTomUnHappy] = useHappy(false)

枚举

enum  是对 JS 标准数据类型的一种补充.

1
2
3
4
enum Color {Red, Green, Blue}
//有一种数据类型是Color,这种类型有三种参数
let c: Color = Color.Green
//设定c是Color类型中的Green

默认情况下,从 0  开始为元素符号. 可以手动赋值

1
2
3
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;
console.log(c) //2,初始值设定为1,那么其余成员会从1开始增长

然后可以由枚举值获得它的名字.

1
2
3
enum Color {Red = 1, Green, Blue}
let c: Color = Color[2]
console.log(c) //显示"Green"

Any

用于暂时不清楚类型的指定类型.
当只知道一部分数据类型时, any  类型也是有用的.

1
2
let list: any = [1, true, "free"];
list[1] = 100;

Void

void  类型表示与 any  类型相反.它表示没有任何类型,
当一个函数没有返回值时,通常其返回值类型是 void .
声明一个void类型的变量没有什么大用,因为你只能为它赋予undefinednull

1
let unusable: void = undefined;

Null 和 Undefined

TypeScript 里,undefinednull两者各自有自己的类型分别叫做undefinednull。 和  void相似,它们的本身的类型用处不是很大:

1
2
let u: undefined = undefined;
let n: null = null;

默认情况下nullundefined是所有类型的子类型。 就是说你可以把  nullundefined赋值给number类型的变量。

然而,当你指定了--strictNullChecks标记,nullundefined只能赋值给void和它们各自。 这能避免 很多常见的问题。 也许在某处你想传入一个 stringnullundefined,你可以使用联合类型string | null | undefined。 再次说明,稍后我们会介绍联合类型。
注意:我们鼓励尽可能地使用--strictNullChecks,但在本手册里我们假设这个标记是关闭的。

Never

never  类型表示那些永不存在的类型.例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是  never类型,当它们被永不为真的类型保护所约束时。

never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使  any也不可以赋值给never

1
2
3
4
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}

Object

object  表示非原始类型.也就是除 number , string , boolean , symbol , null , undefined  之外的类型.
用来表示对象类型。

1
2
3
4
5
declare function create(o: object | null): void;
create({ prop: 0 }); //ok
create(null); //ok

create(42); //Error

类型断言

类型断言类似于类型转换,不进行特殊的数据检查和解构.
没有运行时的影响,只在编译阶段起作用.
Typescript  假设已进行了必须的检查.

类型断言的形式

  1. 尖括号
1
2
let someValue: any = "this is a string"
let strLength: number = (<string>someString).length
  1. as  语法
1
2
let someValue: any = "this is a stirng"
let strLength: number = (someValue as string).length

我们传给setTimeout的每一个函数表达式实际上都引用了相同作用域里的同一个i

接口 interface

接口的作用就是为类型命名和三方代码定义契约.

可选属性

带可选属性的接口在可选属性名字定义的后面加 ?  符号.

好处一: 对可能存在的属性进行预定义.
好处二: 可以捕获引用了不存在属性时的错误.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string, area: number } {
let newSquare = { color: "white", area: 100 };
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}

return newSquare;
}
let mySquare = createSquare({ color: "black" });

可选参数

1
2
3
4
5
interface Person {
name: string;
age?: string;
[propName: string]: any;
}

只读属性

一些对象属性只能在对象刚刚创建时修改其值.
可以在属性名前加 readonly  指定只读属性.

1
2
3
4
interface Point {
readonly x: number;
readonly y: number;
}

通过赋值一个对象字面量来构造 Point .赋值后, x  和 y  不能再被改变.

1
2
let p1: Point = { x: 10, y: 20 };
p1.x = 5; //Error

TypeScript 具有 ReadonlyArray<T>  类型,与 Array<T>  相似.
只是把所有可变方法去掉了.可以确保数组创建后再也不会被修改.

1
2
3
4
5
let a: number[] = [1, 2, 3];
let ro: ReadonlyArray<number> = a;
ro[0] = 2; //error
ro.push(4); //error
a = ro; //error

ReadonlyArray  赋值到一个普通数组也不行.
但是可以用类型断言重写.

1
a = ro as number[]

readonly  和 const

最简单判断该用 readonly 还是 const 的方法是看要把它做为变量使用还是做为一个属性。  做为变量使用的话用  const,若做为属性则使用 readonly。

额外的属性检查

如可选属性上的例子,如果

1
let mySquare = createSquare({ colour: "red", width: 100 }); //error

额外的属性检查会报错.

绕过属性检查

  1. 使用类型断言
1
let mySquare = createSquare({ width: 100, opacity: 0.5}) as SquareConfig;
  1. 添加字符串索引签名
1
2
3
4
5
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
  1. 将这个对象赋值给另一个变量
1
2
let SquareOptions = { colour: "red", width: 10 };
let mySquare = createsquare(squareOptions);

因为  squareOptions 不会经过额外属性检查,所以编译器不会报错。

函数类型

给接口定义一个调用签名就可以使用接口表示函数类型.

1
2
3
interface SearchFunc {
(source: string, substring: string): boolean;
}

展示创建一个函数类型的变量,并将同一类型的函数赋值给这个变量.

1
2
3
4
5
let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
let result = src.search(subString);
return result > -1;
};

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。  比如,我们使用下面的代码重写上面的例子:

1
2
3
4
5
let mySearch: SearchFunc;
mySearch = function (src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
};

可索引的类型

指能够”通过索引得到”的类型.

1
2
3
4
5
6
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myAray = ["Bob", "Fred"];
let myStr: string = myArray[0];

上面例子,定义了 StringArray  接口,具有索引签名.
这个索引签名表示当用 number  去索引 StringArray  时会得到 String  类型的返回值.

TypeScript 支持两种索引签名:字符串和数字。  可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。  这是因为当使用  number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。  也就是说用  100(一个 number)去索引等同于使用”100”(一个 string)去索引,因此两者需要保持一致。

可以将索引签名设置为只读,这样就防止了给索引赋值:

1
2
3
4
5
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"]
mArray[2] = "Marry" //error

你不能设置 myArray[2],因为索引签名是只读的。

类类型

实现接口

与 C#或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约。

1
2
3
4
5
6
7
8
interface ClockInterface {
currentTime: Date;
}

class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) {}
}

也可以在接口中描述一个方法,在类里实现,

1
2
3
4
5
6
7
8
9
10
11
12
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}

class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number){ }
}

接口描述了类的公共部分,而不是公共和私有两部分。  它不会帮你检查类是否具有某些私有成员。

类静态部分与实例部分的区别(没看懂)

当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。  你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:
这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor 存在于类的静态部分,所以不在检查的范围内。
因此,我们应该直接操作类的静态部分。  看下面的例子,我们定义了两个接口, ClockConstructor 为构造函数所用和 ClockInterface 为实例方法所用。  为了方便我们定义一个构造函数  createClock,它用传入的类型创建实例。(没看懂)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

因为 createClock 的第一个参数是 ClockConstructor 类型,在 createClock(AnalogClock, 7, 32)里,会检查 AnalogClock 是否符合构造函数签名。

继承接口

和类一样,接口也可以继承.

1
2
3
4
5
6
7
8
9
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{}
square.color = "blue";
square.sideLength = 10;

一个接口可以继承多个接口,创建出多个接口的合成接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Shape {
color: string;
}

interface PenStroke {
penWidth: number;
}

interface Square extends Shape, PenStroke {
sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

混合类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;

接口继承类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Control {
private state: any;
}

interface SelectableControl extends Control {
select(): void;
}

class Button extends Control implements SelectableControl {
select() { }
}

class TextBox extends Control {
select() { }
}

// 错误:“Image”类型缺少“state”属性。
class Image implements SelectableControl {
select() { }
}

class Location {

}

在上面的例子里,SelectableControl 包含了 Control 的所有成员,包括私有成员 state。  因为  state 是私有成员,所以只能够是 Control 的子类们才能实现 SelectableControl 接口。  因为只有  Control 的子类才能够拥有一个声明于 Control 的私有成员 state,这对私有成员的兼容性是必需的。
在 Control 类内部,是允许通过 SelectableControl 的实例来访问私有成员 state 的。  实际上, SelectableControl 接口和拥有 select 方法的 Control 类是一样的。 Button 和 TextBox 类是 SelectableControl 的子类(因为它们都继承自 Control 并有 select 方法),但 Image 和 Location 类并不是这样的

类型别名 type

大部分时候,类型别名 type 和接口 interface 可以互换.但是有些时候只能使用类型别名.
比如,联合类型.

1
2
3
4
5
6
7
8
9
10
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");

类的关键字

  • public
  • private 类的外部不可用,继承也不行
  • protected 类的外部不可用,继承可以
  • public readOnly xxx 只读属性
  • static funcXXX 静态方法,不需要 new 就可以调用
  • abstract funcXXX 抽象类,所有子类都必须要实现 funcXXX

类有 3 个成员: greeting  内部参数, constructor  构造函数, greet()  方法

继承

使用继承来扩展类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//基类
class Animal {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m`);
}
}
类;
class Dog extends Animal {
bark() {
console.log("aa");
}
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

类从基类中继承了属性和方法.
其中, Dog  是个派生类,它派生自 Animal  基类,通过 extends  关键字.
派生类通常被称为子类, 基类为超类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Animal {
name: string;
constructor(theName: string) {
this.name = theName
}
move(distanceInMeters: number = 0){
console.log(`${this.name} moved ${distanceInMeters}m`)
}
class Snake extends Animal {
constructor(name: string) {
super(name)
}
move(distanceInMeters = 5){
console.log("Slitering...")
super.move(distanceInMeters)
}
}
class Horse extends Animal {
constructor(name: string){
super(name)
}
move(distanceInMeters = 40){
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let sam = new Snake('Python');
let tom: Animal = new Horse('Palo');
sam.move();
tom.move(34);

派生类 Snake  包含一个 super() ,它会执行基类的构造函数.
在构造函数里访问 this  的属性之前,一定要调用 super .

Snake 类和  Horse 类都创建了  move 方法,它们重写了从  Animal 继承来的  move 方法,使得  move 方法根据不同的类而具有不同的功能。  注意,即使  tom 被声明为  Animal 类型,但因为它的值是  Horse,调用  tom.move(34)时,它会调用  Horse 里重写的方法

公有,私有与受保护的修饰符

默认为 public

Ts 中.成员默认为 public ,

private

当成员被标记为 private ,就不能在声明它的外部访问.

1
2
3
4
5
6
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
new Animal("Cat").name //error, name是私有的.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
class Rhio extends Animal {
constructor() {
super("Rhio");
}
}
class Employee {
private name: string;
cosntructor(theName: string) {
this.name = theName;
}
}
let animal = new Animal("Goat");
let rhio = new Rhio();
let employee = new Employee("Bob");

animal = rhio;
animal = employee; //error,Animal和Employee不兼容.

Rhio  继承了 Animal ,它们共享了私有定义 private name: string ,因此兼容.
Emploee  虽然也有私有定义 name ,但是和 Animal  定义的并不相同.

protected

protected 修饰符与  private 修饰符的行为很相似,但有一点不同, protected 成员在派生类中仍然可以访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
protected name: string;
constructor(name: string) {
this.name = name;
}
}

class Employee extends Person {
private department: string;

constructor(name: string, department: string) {
super(name);
this.department = department;
}

public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误

注意,我们不能在  Person 类外使用  name,但是我们仍然可以通过  Employee 类的实例方法访问,因为  Employee 是由  Person 派生而来的。

构造函数也可以被标记成  protected。  这意味着这个类不能在包含它的类外被实例化,但是能被继承

readonly 修饰符

你可以使用  readonly 关键字将属性设置为只读的。  只读属性必须在声明时或构造函数里被初始化。

参数属性

参数属性通过给构造函数参数前面添加一个访问限定符来声明。
在构造函数里使用 readonly name:string  参数来创建和初始化 name  成员.
对于 private  和 protected  也一样.

存储器

TS 支持通过 getters/setters  来截取对对象成员的访问.
将下面的例子改写为使用 get  和 set .

1
2
3
4
5
6
7
8
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let passcode = "secret passcode";
class Employee {
private _fullName: string;

get fullName(): string {
return this._fullName();
}
set fullName(newName: string) {
if (passcode && passcode == "secret passcode") {
this._fullName = newName;
} else {
console.log("Error");
}
}
}

let empolyee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
alert(employee.fullName);
}

修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限.

抽象类

抽象类一般不会被实例化.不同接口,抽象类可以包含成员的细节.
abstract  用于定义抽象类和在抽象类内部定义抽象方法.

1
2
3
4
5
6
7
8
9
10
11
12
abstract class Department {
constructor(public name: string) {
}
printName(): void {
console.log('department name: ' + this.name);
}
abstract printMeeting(): void; //必须在派生类中实现
}

class AccountingDepartment extends Department {

}

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。  抽象方法的语法与接口方法相似。  两者都是定义方法签名但不包含方法体。  然而,抽象方法必须包含  abstract 关键字并且可以包含访问修饰符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
abstract class Department {
constructor(public name: string) {}

printName(): void {
console.log("Department name: " + this.name);
}

abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {
constructor() {
super("Accounting and Auditing"); // 在派生类的构造函数中必须调用 super()
}

printMeeting(): void {
console.log("The Accounting Department meets each Monday at 10am.");
}

generateReports(): void {
console.log("Generating accounting reports...");
}
}

let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在

把类当做接口用

类定义会创建两个东西:类的实例类型和一个构造函数。  因为类可以创建出类型,所以你能够在允许使用接口的地方使用类

1
2
3
4
5
6
7
8
9
10
class Point {
x: number;
y: number;
}

interface Point3d extends Point {
z: number;
}

let point3d: Point3d = { x: 1, y: 2, z: 3 };

单例模式

使用instance关键字.
constructor私有化.
在类的内部去 new 一个实例,在外部无法 new 该实例.

1
2
3
4
5
6
7
8
9
10
11
12
class DellAnalyzer implements IAnalyzer {
private static instance: DellAnalyzer
// 使用该函数提供对外的方法入口
static getInstance() {
if (!DellAnalyzer.instance) {
DellAnalyzer.instance = new DellAnalyzer()
}
return DellAnalyzer.instance
}
// 改成单例模式,私有化constructor,外部无法调用
private constructor() { }
...

装饰器

类的装饰器

装饰器本身是一个函数,通过@符号使用.
接收的参数是一个构造函数.
执行时机在类创建时立即执行.
多个装饰器,执行顺序是反着的,先写的后执行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function testDecorator() {
// 使用泛型规范入参为构造函数
return function <T extends new (...args: any[]) => any>(constructor: T) {
// 扩展constructor
return class extends constructor {
name = "lee";
getName() {
return this.name;
}
};
};
}

const Test = testDecorator()(
class {
name: string;
constructor(name: stirng) {
this.name = name;
}
}
);
const test = new Test("dell");
console.log(test.getName);

类中方法的装饰器

普通方法,target 对应的是类的prototype
静态方法,target 对应的是类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function getNameDecorator(
target: any,
key: string,
descriptor: PropertypeDecorator
) {}
class Test {
name: string;
constructor(name: string) {
this.name = name;
}
@getNameDecorator
getName() {
return this.name;
}
}

const test = new Test("dell");
consoele.log(test.getName());

类中访问器的装饰器

getset不能同时使用装饰器.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function visitDecorator(
target: any,
key: string,
descriptor: PropertyDecorator
) {}
class Test {
private _name: string;
constructor(name: string) {
this._name = name;
}
get name() {
return this._name;
}
@visitDecorator
set name(name: string) {
this._name = name;
}
}
const test = new Test("dell");
test.name = "dell lee";
console.log(test.name);

类中属性的装饰器

和方法的装饰器的区别是没有第三个参数,但是可以自己添加

装饰器的修改是修改 prototype 上的,即修改并不是实例上的,而是原型上的.

1
2
3
4
5
6
7
8
9
10
11
12
13
function nameDecorator(target: any, key: string): any {
const descriptor: PropertyDecorator = {
writable: false,
};
return descriptor;
}
class Test {
@nameDecorator
name = "dell";
}
const test = new Test();
test.name = "dell lee";
console.log(test.name); //报错

类中参数的装饰器

1
2
3
4
5
6
7
8
9
10
function paramDecorator(target: any, method: string, paramIndex: number) {
console.log(target, method, paramIndex);
}
class Test {
getInfo(@paramDecorator name: string, age: number) {
console.log(name, age);
}
}
const test = new Test();
test.getInfo("Dell", 30);

示例

将报错捕获统一使用装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 工厂模式返回一个装饰器
function catchError(msg: string) {
return function (target: any, key: string, discriptor: propertyDecorator) {
const fn = discriptor.value;
descriptor.value = function () {
try {
fn();
} catch (e) {
console.log(msg);
}
};
};
}
class Test {
@catchError("userInfo.name不存在")
getName() {
return userInfo.name;
}
@catchError("userInfo.age不存在")
getAge() {
return userInfo.age;
}
}
const test = new Test();
test.getName();
test.getAge();

函数

函数类型

函数类型包括两部分: 参数类型和返回值类型.

1
2
3
4
5
6
let myAdd: (x: number, y: number) => number = function (
x: number,
y: number
): number {
return x + y;
};

只要参数类型匹配,就认为他是有效的函数类型,不在乎参数名是否正确.

在函数和返回值类型之前使用( => )符号.

返回值类型是函数类型的必要成分,如果函数没有返回任何值.也必须指定返回值类型为 void .

推断类型

如果在赋值语句一边指定类型,另一边没有指定.TS 会自定识别处类型:

1
2
3
4
5
6
7
8
9
// myAdd has the full function type
let myAdd = function (x: number, y: number): number {
return x + y;
};

// The parameters `x` and `y` have the type number
let myAdd: (baseValue: number, increment: number) => number = function (x, y) {
return x + y;
};

这叫’按上下文归类’,是类型推论的一种.

可选参数和默认参数

TypeScript 里的每个函数参数都是必须的。  这不是指不能传递  null 或 undefined 作为参数,而是说编译器检查用户是否为每个参数都传入了值。  编译器还会假设只有这些参数会被传递进函数。  简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。

1
2
3
4
5
function buildName(firstName: string, lastName: string;) {
return firstName + " " + lastName
}
let result1 = buildName("Bob"); //error, 少参数
let result2 = buildName("Bob",

剩余参数

若要同时操作多个参数,可以使用arguments来访问所有传入的参数.

1
2
3
4
function buildName(firstName: string, ...restOfName: string[]){
return firstName + ' ' + restOfName.join(" ")
}
let employeeName = buildName("Jose", "Sam" ,"Lucas", "Mack"

this

在箭头函数中指定this参数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// NOTE: The function now explicitly specifies that its callee must be of type Deck
createCardPicker: function (this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);

return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
};
},
};

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

重载

为同一个函数提供多个函数类型定义来进行函数重载.
编译器会根据这个列表去处理函数的调用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
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)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

为了让编译器能够选择正确的检查类型,它与 JavaScript 里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。

注意,function pickCard(x): any 并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用 pickCard 会产生错误。

泛型

使用<>来指定泛型的类型,保证传入的参数和输出的参数是一致的.
更普遍的泛型是不写,让编译器自己通过类型推断确定类型,如果编译器无法推断,就需要写泛型了.
泛型是声明的时候随便定义,使用的时候进行真正定义.

1
2
3
4
5
6
7
8
function join<T, P>(first: T, second: P) {
return `${first}${second}`;
}
// 声明的时候并不知道具体参数是什么类型
// 但是使用时, 通过在这里声明就限定了参数的类型
join<number, string>(1, "1");
// 当然也可以不写,这样会自己推断出
join(1, "1");

使用泛型变量

通过指定 arg 的参数类型是 T 的数组,返回的元素类型也是 T 的数组,存在长度,不会报错.

1
2
3
4
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

泛型类型

泛型类

类似于泛型接口,泛型类使用<>括起泛型类型,跟在类名后面.

1
2
3
4
5
6
7
8
9
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
}
1
2
3
4
5
6
7
8
9
class DataManager<T> {
constructor(private data: T[]) {}
getItem(index: number): T {
return this.data[index];
}
}
// 调用时指定泛型是number
const data = new DataManger<number>(["1"]);
data.getItem(0);

此时 GenericNumber 类只能使用 number 类型.
类有两部分:静态部分和实例部分.泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型.

泛型约束

通过extends关键字约束条件.
定义一个接口来描述约束条件,创建一个包含.length属性的接口.

1
2
3
4
5
6
7
interface Lengthwise {
length: number
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length)
retrun arg
}

枚举

作用:使用枚举定义一些带名字的常量.

数字枚举

1
2
3
4
5
6
7
8
enum Direction {
Up = 1,
Down,
Left,
Right,
}
//Up初始化为1,其余成员从1开始增长
//如果不设置初始值,那么up为0,总之,各个值都是不同的

用法: 通过枚举的属性来访问枚举的成员,和枚举的名字来访问枚举类型:

1
2
3
4
5
6
7
8
enum Response {
No = 0,
Yes = 1,
}
function respond(recipient: string, message: Response): void {
//...
}
respond('Princess Caroline', Response.Yes)

不带初始化器的枚举或者被放在第一的位置,或者被放在使用了数字常量或其他常量初始化了的枚举后面.

字符串枚举

在一个字符串枚举中,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化.

异构枚举

枚举可以混合字符串和数字成员.但不建议这样做.

计算的和常量成员

每个枚举成员都带有一个值,他可以是常量或计算出来的.当满足如下条件时,枚举成员被当做是常量:

  1. 它是枚举的第一个成员且没有初始化器,这种情况下它被赋予值 0:
1
enum E { x }
  1. 它不带有初始化器且它之前的枚举成员是一个数字常量.这种情况下,当前枚举成员的值为它上一个枚举成员的值加 1.
1
2
3
enum E {
A =1, B, C
}
  1. 枚举成员使用常量枚举表达式初始化.常数枚举表达式是 TS 表达式的子集.它可以在编译阶段求值.当一个表达式满足下面条件之一时,它就是一个常量枚举表达式:
  • 一个枚举表达式字面量
  • 一个对之前定义的常量枚举成员的引用
  • 带括号的常量枚举表达式
  • 一元运算符+,-,~其中之一应用在常量枚举表达式
  • 常量枚举表达式作为二次运算符+,-,*,/,%,<<,>>,>>>,&,|,^的操作对象,若常熟枚举表达式求值后为NaNInfinity,则会在编译阶段报错.

所有其他情况的枚举成员被当做是需要计算得出的值.

1
2
3
4
5
6
7
enum FileAccess {
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
G = '123'.length
}

联合枚举与枚举成员的类型

字面量枚举成员是指不带有初始值的常量枚举成员,或者值被初始化为

  • 任何字符串字面量
  • 任何数字字面量
  • 应用了一元-符号的数字字面量

当所有枚举成员都拥有字面量枚举值时,他就带有了一种特殊的语义.
首先,枚举成员成为了类型.
枚举类型本身变成了每个枚举成员的联合.

运行时枚举

枚举是在运行时真正存在的对象.

1
2
3
enum E {
X,Y,Z
}

反向映射

1
2
3
4
5
enum Enum {
A
}
let a = Enum.A
let nameOfA = Enum[a] //"A"

const 枚举

为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问.
可以使用 const 枚举,常量枚举通过在枚举上使用const 修饰符来定义.

1
2
3
4
const enum Enum {
A = 1,
B = A * 2
}

常量枚举只能使用常量枚举表达式,并且不同于常规枚举,它们在编译阶段会被删除,常量枚举成员在使用的地方会被内联进来.原因是常量枚举不允许包含计算成员.

1
2
3
4
5
6
7
8
const enum Directions {
Up,
Down,
Left,
Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]

生产的代码为

1
var directions = [0, 1, 2, 3];

外部枚举

描述已经存在的枚举类型的形状.

1
2
3
4
5
declare enum Enum {
A = 1,
B,
C =2
}

类型推论

上下文类型

TS 的类型推论可能按照相反的方向进行.叫做按上下文归类.

1
2
3
window.onmousedown = function (mouseEvent) {
console.log(mouseEvnet.button); //error
};

这个例子会得到一个类型错误.TS 类型检查器使用Window.onmousedown函数的类型来推断右边函数表达式的类型.如果函数表达式不是在上下文类型的位置,mouseEvent参数的类型需要指定为any,这样也不会报错.

类型兼容性

开始

结构化类型系统的基本规则是,如果x想兼容y,那么y至少具有与x相同的属性.

1
2
3
4
5
6
7
interface Named {
name: string;
}
let x: Named;
let y = { name: "Alice", location: "Seattle" };
//类型推断y的类型是{name: string, location: string}
x = y;

这里检查 y 是否可以赋值给 x,编译器检查 x 中的每个属性,看是否能在 y 中也找到对应属性.注意,y 有个额外参数,但不会引起错误.只有目标类型(这里是 Named)的成员会被一一检查是否兼容.

比较函数

1
2
3
4
let x = (a: number) => 0;
let y = (a: number, s: string) => 0;
y = x; //ok
x = y; //Error

看 x 是否可以赋值给 y 首先看参数列表.
x 的每个参数必须在 y 里找到对应类型的参数.注意的是参数的名字相同与否无所谓,只看类型.
x 的每个参数都可以在 y 中找到对应的参数,所以运行赋值.
第二个赋值错误,因为 y 有个必须的第二个参数,但是 x 没有,所以不允许赋值.

枚举

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容.不同枚举类型之间是不兼容.

1
2
3
4
5
enum Status { Ready, Waiting}
enum Color { Red, Blue,Green }

let status = Status.Ready
status = Color.Green //Error

类与对象字面量和接口差不多,但有一点不同: 类有静态部分和实例部分的类型.
比较两个类类型的对象时,只有实例的成员会被比较.静态成员和构造函数不在比较的范围内.

泛型

因为 TS 是结构性的类型系统,类型参数只影响使用其作为类型一部分的结果类型.

1
2
3
4
interface Empty<T> {
let x: Empty<number>;
let y: Empty<string>;
x = y; //OK

其中,x 和 y 是兼容的,因为他们的结构使用类型参数时并没有什么不同.

高级类型

交叉类型

即多个类型合并成一个类型.
适合场景: 混入或者其他不适合典型面向对象模型的地方.
下面的例子没看懂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{};
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}

class Person {
constructor(public name: string) { }
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
// ...
}
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

联合类型

表示一个值可以是几种类型之一.我们用竖线(|)分隔每个类型.
可以访问联合类型的所有类型里的共有成员.

类型保护与区分类型

1
2
3
4
5
6
7
8
//下面代码会报错
let pet = getSmallPet();
//每一个成员访问都会报错
if (pet.swim) {
pet.swim();
} else if (pet.fly) {
pet.fly();
}

可以使用类型断言解决:

1
2
3
4
5
6
let pet = getSmallPet()
if((<Fish>pet).swim){
(<Fish>pet).swim()
}else{
(<Bird>pet).fly()
}

声明文件

全局库声明文件

通过declare关键字进行声明文件可以在全局访问,而一般将该文件定义在types/*.d.ts中方便管理.
interfacetype不需要declare也可以在全局访问.
一般的declare声明不能重复声明,即使重复也只显示第一个.而interface重复声明会发生合并.
declare使用export替换效果是一样的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// jQuery.d.ts
// 定义全局变量
declare var $: (param: () => void) => void;
// 定义全局函数
interface JQueryInstance {
html: (html: string) => JqueryInstance;
}

// 函数重载(也就是重复声明)
declare function $(readyFunc: () => void): void;
declare function $(selector: string): JqueryInstance;
// 使用interface定义
interface JQuery {
(readyFunc: () => void): void;
(selector: string): JqueryInstance;
}
declare var $: JQuery;

namespace

1
2
3
4
5
6
7
8
declare let n: number = 123; //可以全局访问
declare namespace myLib {
function making(s: string): string;
let numbeOfMaking: number;
}
// 可以将这些变量放在namespace的作用域内,
// 通过myLib.making()进行访问.
// 内部不用再写declare,另外内部也可以嵌套namespace

模块化库声明文件

模块化声明时文件放置到types/*,声明该模块同名的文件夹.
注意修改tsconfig.json文件中的配置

1
2
3
4
5
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"esModuleInterop": true,

泛型帮助类型

类型保护

通过类型推论进行类型保护,即类型收窄.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Pet {
name: string;
}

interface Fish extends Pet {
swim(): void;
}

interface Bird extends Pet {
fly(): void;
}
function isFish(pet: Pet): pet is Fish {
return (pet as Fish).swim !== undefined;
}

pets.forEach((pet) => {
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
});

in 操作符

同样用于类型收窄(类型保护)

1
2
3
4
5
6
7
pets.forEach((pet) => {
if ("swim" in pet) {
pet.swim();
} else {
pet.fly();
}
});

typeof 操作符

typeof variable === '类型名称'表达式明确告知 TypeScript 变量 variable 的类型,
起到类型保护的作用.

instanceof 操作符

variable instanceof Type告知 TypeScript 变量 variable 的类型为 Type,起到类型保护的作用:
使用instanceof注意只能使用class,不能使用interface.否则无法调用instanceof.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Fish {
swim() {
console.log("fish is swiming");
}
}

class Bird {
fly() {
console.log("bird is flying");
}
}

function move(pet: Fish | Bird) {
if (pet instanceof Fish) {
pet.swim();
} else {
pet.fly();
}
}

关键字

extends

T extends U ? X : Y;
若 T 能赋值给 U,则类型是 X,否则是 Y.

1
2
3
4
type Words = "a" | "b" | "c";
type W<T> = T extends Words ? true : false;
type WA = W<"a">; // => true
type WD = W<"d">; // => false

infer

表示在 extends 条件语句中待推断的类型变量.i

1
type Union<T> = T extends Array<infer U> ? U : never;
  • 如果泛型参数 T 满足约束条件 Array,那就返回这个类型变量 U.
1
2
3
4
5
6
7
8
type ParamType<T> = T extends (param: infer P) => any ? P : T;
interface IDog {
name: string;
age: number;
}
type Func = (dog: IDog) => void;
type Param = ParamType<Func>; //IDog
type TypeSring = ParamType<stirng>; //string

keyof(索引类型)

用来取得一个对象接口的所有 key 值.
keyof 可以获取某种类型的所有键,返回类型是联合类型。

1
2
3
4
5
6
7
8
interface IPerson {
name: string;
age: number;
sex?: string;
}
type K1 = keyof IPerson; //'name' | 'age' | 'sex'
type K2 = keyof IPerson[]; //'length'|'push'|'pop'...
type K3 = typeof { [x:string]: Person }; //string | number`

typeof

在 JS 中,typeof 判断数据类型.
在 TS 中,获取一个变量的声明类型,如果不存在,获取该变量的推论类型.
typeof 可以获取一个变量或者对象(包括函数)的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface IPerson {
name: string;
age: number;
sex?: string;
}

const jack: IPerson = { name: "jack", age: 100 };
type Jack = typeof jack; // -> IPerson

function foo(x: number): Array<number> {
return [x];
}

type F = typeof foo; // -> (x: number) => number[]
//Jack 这个类型别名实际上就是 jack 的类型 IPerson,
//而 F 的类型就是 TS 自己推导出来的 foo 的类型 (x: number) => number[]。

内置帮助类型

Partial(映射类型)

让 T 中所有属性都是可选的.

1
2
3
type Partial<T> = {
[P in keyof T]?: T[P];
};

在某些情况下,我们希望类型中的所有属性都不是必需的,只有在某些条件下才存在,我们就可以使用 Partial 来将已声明的类型中的所有属性标识为可选的。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Dog {
age: number;
name: string;
pirce: number;
}

type PartialDog = Partial<Dog>;
//等价于
type PartialDog = {
age?: number;
name?: string;
pirce?: number;
};

Required

Partial正好相反,所有属性都是必选的.

Readonly(映射类型)

所有属性设为只读

Record<K,T>

1
2
3
4
5
6
7
8
/**
* Construct a type with a set of properties K of type T
* 构造一个具有一组属性K(类型T)的类型
*/

type Record<K extends keyof any, T> = {
[P in K]: T;
};

构造一个具有一组属性 K(类型 T)的类型.
K 对应 key,T 对应对象的 value.返回是一个声明好的对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type petsGroup = "dog" | "cat";
interface IPetInfo {
name: string;
age: number;
}

type IPets = Record<petsGroup, IPetInfo>;

const animalsInfo: IPets = {
dog: {
name: "wangcai",
age: 2,
},
cat: {
name: "xiaobai",
age: 3,
},
};

Pick<T,K>

1
2
3
4
5
6
7
/**
* From T, pick a set of properties whose keys are in the union K
* 从T中,选择一组键在并集K中的属性
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

从源码可以看到 K 必须是 T 的 key,然后用 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值.
相当于从 T 里挑选 K(可能是多个)出来作为属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface IDog {
name: string;
age: number;
height: number;
weight: number;
}

type PickDog = Pick<IDog, "name" | "age" | "height">;
// 等价于
type PickDog = {
name: string;
age: number;
height: number;
};

let dog: PickDog = {
name: "wangcai",
age: 3,
height: 70,
};

Exclude<T,U>

排除 T 中的 U.

1
2
3
4
5
/**
* Exclude from T those types that are assignable to U
* 从T中排除那些可分配给U的类型
*/
type Exclude<T, U> = T extends U ? never : T;

与 Pick 相反,Pick 用于拣选出我们需要关心的属性,而 Exclude 用于排除掉我们不需要关心的属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
interface IDog {
name: string;
age: number;
height: number;
weight: number;
sex: string;
}

type keys = keyof IDog; // -> "name" | "age" | "height" | "weight" | "sex"

type ExcludeDog = Exclude<keys, "name" | "age">;
// 等价于
type ExcludeDog = "height" | "weight" | "sex";

Extract<T,U>

相当于取交集

1
2
3
4
5
/**
* Extract from T those types that are assignable to U
* 从T中提取可分配给U的类型
*/
type Extract<T, U> = T extends U ? T : never;

Omit<T,K>

类似于Exclude,但是更方便.
用于保留一些属性,再排除一些属性.

1
2
3
4
5
/**
* Construct a type with the properties of T except for those in type K.
* 构造一个除类型K之外的T属性的类型
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

NonNullable

1
2
3
4
5
/**
* Exclude null and undefined from T
* 从T中排除null和undefined
*/
type NonNullable<T> = T extends null | undefined ? never : T;

Parameters

返回类型为 T 的函数的参数类型所组成的数组.
也就是将参数取出来放进数组里.

1
2
3
4
5
6
7
8
9
/**
* Obtain the parameters of a function type in a tuple
* 在元组中获取构造函数类型的参数
*/
type Parameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
1
2
type T0 = Parameters<() => string>; // []
type T1 = Parameters<(s: string) => void>; // [string]

ConstructorParamters

1
2
3
4
5
6
/**
* Obtain the parameters of a constructor function type in a tuple
* 在元组中获取构造函数类型的参数
*/
type ConstructorParameters<T extends new (...args: any) => any> =
T extends new (...args: infer P) => any ? P : never;

ReturnType

1
2
3
4
5
6
7
8
9
/**
* Obtain the return type of a function type
* 获取函数类型的返回类型
*/
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;

InstanceType

1
2
3
4
5
6
7
8
9
10
/**
* Obtain the return type of a constructor function type
* 获取构造函数类型的返回类型
*/

type InstanceType<T extends new (...args: any) => any> = T extends new (
...args: any
) => infer R
? R
: any;

ThisType

1
2
3
4
5
/**
* Marker for contextual 'this' type
* 上下文“this”类型的标记
*/
interface ThisType<T> {}
1
2
3
4
5
6
7
8
9
interface Cat {
name: string;
age: number;
}
const obj: ThisType<Person> = {
mimi() {
this.name; // string
},
};

这样的话,就可以指定 obj 里的所有方法里的上下文对象改成 Person 这个类型了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 没有ThisType情况下
const dog = {
wang() {
console.log(this.age); // error,在dog中只有wang一个函数,不存在a
},
};
// 使用ThisType
const dog: { wang: any } & ThisType<{ age: number }> = {
wang() {
console.log(this.wang); // error,因为没有在ThisType中定义
console.log(this.age); // ok
},
};
dog.wang; // ok 正常调用
dog.age; // error,在外面的话,就跟ThisType没有关系了,这里就是没有定义age了

ThisType 的作用是:提示其下所定义的函数,在函数 body 中,其调用者的类型是什么。

命名空间

在命名空间中,将想要暴露出去的代码使用export暴露出去,即可在外部调用.

补充

三斜线指令

1
///<reference path="...' />

表示引入文件,只能存在于最前端,在后面会被认为是注释.

书写建议

  1. 避免 enum 枚举
  2. 避免命名空间 namespace
  3. 避免装饰器,尽量等到这个语法标准化完成。如果你需要一个库用装饰器,要考虑它的标准化状态。
  4. 尽量用 #somePrivateField而不是private somePrivateField.