TypeScript学习笔记

2020/5/16 TypeScript

TIPS

TypeScript学习笔记

# 鸭子类型(Duck Typing)

鸭子类型(英语:duck typing)是动态类型的一种风格,是多态(polymorphism)的一种形式。

在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。

"当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。"

在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型。例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为"鸭子"的对象,并调用它的"走"和"叫"方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的"走"和"叫"方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的"走"和"叫"方法的对象都可被函数接受的这种行为引出了以上表述,这种决定类型的方式因此得名。

# 安装与环境配置

npm install -g typescript
1

配置自动 vscode 自动编译,tsc --init,然后配置 tsconfig.json,设置输出目录,并在终端中运行监视任务

  • 不带任何输入文件的情况下调用 tsc ,编译器会从当前目录开始去查找 tsconfig.json 文件,逐级向上搜索父目录。
  • 不带任何输入文件的情况下调用 tsc ,且使用命令行参数 --project (或 -p ) 指定一个包含 tsconfig.json 文件的目录。

# 基础类型

# 布尔值

最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫做boolean (其它语言中也一样) 。

let isDone: boolean = false;
1

# 数字

和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
1
2
3
4

# 字符串

和JavaScript一样,可以使用双引号 " 或单引号 ' 表示字符串。

let name: string = "bob";
name = "smith";
1
2

模板字符串

let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }.

I'll be ${ age + 1 } years old next month.`;
1
2
3
4
5

# 数组

TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:

let list: number[] = [1, 2, 3];
1

第二种方式是使用数组泛型,Array<元素类型>:

let list: Array<number>  = [1, 2, 3];
1

# 元组 Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 stringnumber 类型的元组。

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error
1
2
3
4
5
6

当访问一个已知索引的元素,会得到正确的类型:

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
1
2

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

x[3] = 'world'; // Error, 长度为 "2" 的元组类型 "[string, number]" 在索引 "3" 处没有元素。

console.log(x[5].toString()); // Error
// 此时,如果改变数组的元素类型或添加元素数量,编辑器都会报错
// TS允许向元组中使用数组的push方法插入新元素(但不允许访问)
x.push('123'); // OK 但是还是无法访问 无法赋值
1
2
3
4
5
6

# 枚举

枚举,取值和对象一致,通过属性访问,key不能是数字,value可以为数字也可以为字符串,分别为数字类型枚举与字符串类型枚举,但是不能为其他值,默认值为0,第一个枚举值或前一个枚举值为数字,可以省略赋值,其值为前一个数字值+1

enum Color {Red, Green, Blue}
let c: Color = Color.Green;
1
2

默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1开始编号:

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;
1
2

或者,全部都采用手动赋值:

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;
1
2

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];

console.log(colorName);  // 显示'Green'因为上面代码里它的值是2
1
2
3
4

# Any

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
1
2
3

# Void

某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void:

function warnUser(): void {
  console.log("This is my warning message");
}
1
2
3

声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null:

let unusable: void = undefined;
1

# Null 和 Undefined

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

// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;
1
2
3

默认情况下 nullundefined 是所有类型的子类型。 就是说你可以把 nullundefined 赋值给 number 类型的变量。 然而,当你指定了 --strictNullChecks 标记,nullundefined 只能赋值给void和它们各自。 这能避免 很多常见的问题。 也许在某处你想传入一个 stringnullundefined ,你可以使用联合类型 string | null | undefined

# 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) {
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# Object

object表示非原始类型,也就是除 numberstringbooleansymbolnullundefined 之外的类型。

使用 object 类型,就可以更好的表示像 Object.create 这样的API。例如:

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
1
2
3
4
5
6
7
8
9

# 类型断言

类型断言有两种形式。 其一是“尖括号"语法:

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;
1
2
3

另一个为as语法:

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;
1
2
3

两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有 as 语法断言是被允许的。 类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的

# 接口

TypeScript里,接口的作用就是为类型命名和为你的代码或第三方代码定义契约。

# 接口初探

function printLabel(labelledObj: { label: string }) {
  console.log(labelledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
1
2
3
4
5
6

类型检查器会查看 printLabel 的调用。 printLabel 有一个参数,并要求这个对象参数有一个名为 label 类型为 string 的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。

重写上面的例子,这次使用接口来描述:必须包含一个label属性且类型为string:

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
1
2
3
4
5
6
7
8
9
10

LabelledValue 接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label 属性且类型为 string 的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给 printLabel 的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。 还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。

# 可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags"模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。

“option bags" 的例子:

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
6
7
8
9
10
11
12
13
14
15
16
17

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ? 符号。 可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。

可选属性不会被标记为undefined,但是赋值是可以的,配置 tsconfig.json"strict": false,或者不写,默认为 false,如果为true那么对于可选属性的类型推断就包含undefined

type A = {
  foo?: string
}
const a: A = {}; // OK
const a1: A = { foo: undefined }; // OK
const a2: A = { foo: null }; // Error, Type 'null' is not assignable to type 'string | undefined'.
console.log(a.foo?.toString()); // OK

type B = {
  foo: undefined | string
}
const b: B = {}; // Error, Property 'foo' is missing in type '{}' but required in type 'B'.
console.log(b.foo?.toString()); // OK, optional chain support undefined | null

type C = {
  foo: undefined
}
const c: C = {}; // Error, Property 'foo' is missing in type '{}' but required in type 'C'.
console.log(c.foo.toString()); // Error, Object is possibly 'undefined'.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly 来指定只读属性:

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

你可以通过赋值一个对象字面量来构造一个 Point。 赋值后, xy 再也不能被改变了。

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

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

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!
1
2
3
4
5
6

上面代码的最后一行,可以看到就算把整个ReadonlyArray赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:

a = ro as number[];
1

# 额外的属性检查

interface SquareConfig {
    color?: string;
    width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
    // ...
}

let mySquare = createSquare({ colour: "red", width: 100 });
1
2
3
4
5
6
7
8
9
10

注意传入 createSquare 的参数拼写为 colour 而不是 color 。 在JavaScript里,这会默默地失败。 TypeScript会认为这段代码可能存在bug。 对象字面量会被特殊对待而且会经过额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型"不包含的属性时,你会得到一个错误。

// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });
1
2

绕开这些检查非常简单。 最简便的方法是使用类型断言:

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
1

然而,最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。 如果 SquareConfig 带有上面定义的类型的 colorwidth 属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}
1
2
3
4
5

# 函数类型

  • 接口能够描述JavaScript中对象拥有的各种各样的外形。
  • 除了描述带有属性的普通对象外,接口也可以描述函数类型。
  • 为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
interface SearchFunc {
  (source: string, subString: string): boolean;
}

// 使用
// 函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配 source subString 可修改
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
}
1
2
3
4
5
6
7
8
9
10
11

# 可索引的类型

与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到"的类型,比如 a[10]ageMap["daniel"] 。 可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];
1
2
3
4
5
6
7
8

定义了 StringArray 接口,它具有索引签名。 这个索引签名表示了当用 number 去索引 StringArray 时会得到 string 类型的返回值。

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;
}
1
2
3
4
5
6
7
8
9
10
11

字符串索引签名能够很好的描述 dictionary 模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.propertyobj["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!
// 你不能设置myArray[2],因为索引签名是只读的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 类类型

interface ClockInterface {
  currentTime: Date;
}

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

// 你也可以在接口中描述一个方法,在类里实现它,如同下面的setTime方法一样:
interface ClockInterface {
  currentTime: Date;
  setTime(d: Date);
}

class Clock implements ClockInterface {
  currentTime: Date;
  setTime(d: Date) {
    this.currentTime = d;
  }
  constructor(h: number, m: number) { }
}
// 接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 类静态部分与实例部分的区别

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
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 继承接口

和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

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

# 混合类型

一个对象可以同时做为函数和对象使用,并带有额外的属性。

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

# 接口继承类

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。接口同样会继承到类的 privateprotected 成员。这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现 ( implement )

当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。 这个子类除了继承至基类外与基类没有任何关系。

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

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

#

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}
let greeter = new Greeter("world");
// 声明一个 Greeter类。这个类有3个成员:一个叫做 greeting的属性,一个构造函数和一个 greet方法。
// 引用任何一个类成员的时候都用了 this。 它表示我们访问的是类的成员。
// 使用 new构造了 Greeter类的一个实例。 它会调用构造函数,创建一个 Greeter 类型的新对象,并执行构造函数初始化它。
1
2
3
4
5
6
7
8
9
10
11
12
13

# 继承

class Animal {
  move(distanceInMeters: number = 0) {
    console.log(`Animal moved ${distanceInMeters}m.`);
  }
}
class Dog extends Animal {
  bark() {
    console.log('Woof! Woof!');
  }
}
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
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("Slithering...");
    super.move(distanceInMeters);
  }
}
class Horse extends Animal {
  constructor(name: string) { super(name); }
  move(distanceInMeters = 45) {
    console.log("Galloping...");
    super.move(distanceInMeters);
  }
}
let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);
// Slithering...
// Sammy the Python moved 5m.
// Galloping...
// Tommy the Palomino moved 34m.

// 派生类包含了一个构造函数,它必须调用 super() ,它会执行基类的构造函数。 而且,在构造函数里访问 this 的属性之前,我们一定要调用 super() 。 这个是TypeScript强制执行的一条重要规则。
// Snake类 和 Horse类 都创建了 move 方法,它们重写了从 Animal继承来的 move方法,使得 move 方法根据不同的类而具有不同的功能。 注意,即使 tom 被声明为 Animal 类型,但因为它的值是 Horse,调用 tom.move(34)时,它会调用 Horse 里重写的方法:
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
31
32

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

默认为 public 当成员被标记成 private时,它就不能在声明它的类的外部访问。比如:

class Animal {
  private name: string;
  constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // 错误: 'name' 是私有的.
1
2
3
4
5

TypeScript使用的是结构性类型系统。当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。 然而,当我们比较带有 privateprotected 成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个 private 成员,那么只有当另外一个类型中也存在这样一个 private 成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected 成员也使用这个规则。

protected 修饰符与 private 修饰符的行为很相似,但有一点不同, protected 成员在派生类中仍然可以访问。 构造函数也可以被标记成 protected 。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。

# readonly修饰符

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

参数属性

class Octopus {
  readonly name: string;
  readonly numberOfLegs: number = 8;
  constructor (theName: string) {
    this.name = theName;
  }
}
// 使用参数属性 效果一样
class Octopus {
  readonly numberOfLegs: number = 8;
  constructor(readonly name: string) {
  }
}
// 参数属性通过给构造函数参数前面添加一个访问限定符来声明。 使用 private 限定一个参数属性会声明并初始化一个私有成员;对于 public和 protected 来说也是一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 存取器

TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。 存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有 get 不带有 set 的存取器自动被推断为 readonly 。 这在从代码生成 .d.ts 文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。

# 静态属性

类的静态成员,这些属性存在于类本身上面而不是类的实例上。 每个实例想要访问静态属性的时候,都要在前面加上类名。 如同在实例属性上使用 this. 前缀来访问属性一样

# 抽象类

抽象类做为其它派生类的基类使用。它们一般不会直接被实例化。不同于接口,抽象类可以包含成员的实现细节。 abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。

abstract class Animal {
  abstract makeSound(): void;
  move(): void {
    console.log('roaming the earch...');
  }
}
1
2
3
4
5
6

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

# 构造函数

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}
let greeter: Greeter; // 意思是 Greeter 类的实例的类型是 Greeter。
greeter = new Greeter("world");
console.log(greeter.greet());
1
2
3
4
5
6
7
8
9
10
11
12

# 把类当做接口使用

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

class Point {
  x: number;
  y: number;
}
interface Point3d extends Point {
  z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
1
2
3
4
5
6
7
8

# 函数

  1. Typescript类型别名

    • 类型别名可以用来给一个类型起一个新名字
    • 采用关键字type例如typeName=string丨number
    • 例子中的Name就表示可设置字符串和数值类型
    • 也可采用type来约束取值只能是某些字符串中的一个:如
    • typeEventNames="click"|"scroll"|"mousemove"
  2. Typescript枚举

    • 枚举(Enum)类型用于取值被限定在一定范围内的场景
    • 采用关键字enum定义,例如:enumDays{Sun,Mon,Tue,Wed,Thu,Fri,Sat}
    • 枚举成员会被赋值为从0开始递增的数字 (默认是从0开始,可以自定义开始的索引值) ,同时也会被枚举值到枚举名进行反向映射, 双向映射的对象
  3. Typescript类的修饰符

    public private 和 protected

    • public修饰的属性或者方法是共有的,可以在任何地方被访问到,默认所有的属性或者方法都是public的
    • private修饰的属性或者方法是私有的,不能在声明它的类外面访问
    • protected修饰的属性或者方法是受保护的,它和private类似

    子类继承父类,在子类里面可以访问父类的public属性,但不能直接访问private和protected属性,可以在子类里面使用super访问

    class Child extends Person{
    callParent() {
      console.log(super.age)
      super.say()
    }
    static test() {
      console.log('test')
    }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    static定义的静态方法可以直接通过类访问,Child.test(),但是里面是禁止使用this;非静态方法必须new之后才能访问

  4. TypeScript泛型

    泛型是指在定义函数、接口或类的时候,不预先指定具体类型,而在使用的时候再指定类型的一种特性

    function createArray<T>(length:number, value:T):Array<T>{
    let arr = []
    for(let i=0;i<length;i++){
      arr[i] = value
    }
    return arr
    }
    var strArr: string [] = createArray<string>(3,'1')
    var numArr: number [] = createArray(3,1)//不传类型时自动倒推返回数组类型
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

类型推断

如果变量的声明和初始化是在同一行,那么可以省略变量类型的声明

let a:number = 1  =>  let a = 1
1

此时a的类型被推断为 number ,后面就只能赋值 number 类型

联合类型:表示取值可以为多种类型之一

let a: string | number = 1
1

# Typescript中的函数

  1. 函数的返回值类型,如果没有返回值则类型定义为void

    function test():string { return '' }
    let tString = test()
    
    1
    2
  2. 函数的形参类型

    function test(a:string, b:number):string { return '' }
    // ts中实参与形参类型必须一致,并且数量也必须一样
    // 可选参数:function test( a?: string):void { ... }
    // 形参默认值, 带默认值的形参默认就是可选参数
    function test(a:string = 'aaa', b:number = 111):void { .. }
    test() // 表示都用默认值
    test('a') // 表示a用实参,b用默认值
    test('a', 1) // 表示都用形参
    test(undefined. 111) // 表示a用默认值 b用形参
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  3. 剩余参数 (形参数量不确定)

    function test( a:number, b:number, ...c: number[] ):void { ... }
    // 剩余参数只能定义一个,并且只能是数组,只能写在形参列表最后··
    
    1
    2
  • 联合类型: 变量: 类型1 | 类型2
  • 交叉类型: 变量: 类型1 & 类型2
interface o1 { x: number, y: number }
interface o2 { z: number }
const obj1: o1 = { x: 1, y: 2 }
const obj2: o2 = { z: 3 }
const obj3: o1 & o2 = Object.assign({}, obj1, obj2)
1
2
3
4
5

字面量类型:有时候我们希望标注的不是某个类型,而是一个固定值,就可以使用字面量类型,与基本类型不同的是它是一个具体的值,可以配合联合类型使用

function setDirection: void (direction: 'left' | 'right' | 'top' | 'bottom') {}
1

类型别名

type dir = 'left' | 'right' | 'top' | 'bottom'
function setDirection: void (direction: dir) {}
1
2

类型推导

初始化变量、设置函数默认参数值、返回函数值 时都会造成ts编辑器根据上下文推导对应的类型标注

let x = 1 // 自动把x推导成number类型
x = '2'  // 报错
1
2

类型断言

  • let b = <类型>a
  • let b = a as 类型

类型操作符

typeof:操作的是值,得到的是值对应的类型

const colors = {
    color1: 'red',
    color2: 'blue'
}
type colorType = typeof colors
// colors {
//     color1: string,
//     color2: string
// }
1
2
3
4
5
6
7
8
9

keyof:获取类型对应的类型的 key 的集合,返回值是 key 的联合类型。注:typeof 操作的是值,keyof 操作的是类型

interface Colors {
   color1: string,
   color2: number
 }
type colorType = keyof Colors
// colorType: 'color1' | 'color2'
let colorData: colorType
colorData = 'color1'
colorData = 'color2'
1
2
3
4
5
6
7
8
9

in:对值和类型都可以使用。针对值进行操作,用作判断值中是否包含指定的 key,针对类型进行操作的话,内部使用 for ... in 对类型进行遍历

in操作符后面的类型必须是 number , string, symbol 三种之一

interface Person {
    name: string,
    age: number
}
type personKey = keyof Person
type newPerson = {
    [k in personKey]: number
    // [key in 'name' | 'age']: number
    // [key in keyof Person]: number
}
1
2
3
4
5
6
7
8
9
10

extend:类型继承操作符

interface type1 {
    x: number,
    y: number,
}
interface type2 extends type1 {}

type type1 = {
    x: number,
    y: number,
}
function fn<T extends type1>(args: T) {}
fn({x:1,y:2})
1
2
3
4
5
6
7
8
9
10
11
12

# 装饰器

使用装饰器存在两个显著的优点:

  • 代码可读性变强了,装饰器命名相当于一个注释
  • 在不改变原有代码情况下,对原来功能进行扩展

后面的使用场景中,借助装饰器的特性,除了提高可读性之后,针对已经存在的类,可以通过装饰器的特性,在不改变原有代码情况下,对原来功能进行扩展

类的装饰器可以装饰:

  • 当装饰器作为修饰类的时候,会把构造器传递进去。

    function addAge(constructor: Function) {
      console.log(constructor); // ƒ Person() { this.name = 'huihui'; }
      constructor.prototype.age = 18;
    }
    
    @addAge
    class Person{
      name: string;
      age!: number;
      constructor() {
        this.name = 'huihui';
      }
    }
    let person = new Person();
    console.log(person.age); // 18
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • 方法/属性

    同样,装饰器可以用于修饰类的方法,这时候装饰器函数接收的参数变成了:

    • target:对象的原型
    • propertyKey:方法的名称
    • descriptor:方法的属性描述符

    可以看到,这三个属性实际就是Object.defineProperty的三个参数,如果是类的属性,则没有传递第三个参数

    // 声明装饰器修饰方法/属性
    function method(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      console.log(target);
      console.log("prop " + propertyKey);
      console.log("desc " + JSON.stringify(descriptor) + "\n\n");
      descriptor.writable = false;
    };
    
    function property(target: any, propertyKey: string) {
      console.log("target", target)
      console.log("propertyKey", propertyKey)
    }
    
    class Person{
    @property
    name: string;
    constructor() {
      this.name = 'huihui';
    }
    
    @method
    say(){
      return 'instance method';
    }
    
    @method
    static run(){
      return 'static method';
    }
    }
    
    const xmz = new Person();
    
    // 修改实例方法say
    xmz.say = function() {
    return 'edit'
    }
    
    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
    31
    32
    33
    34
    35
    36
    37
  • 参数

    接收3个参数,分别是:

    • target:当前对象的原型
    • propertyKey:参数的名称
    • index:参数数组中的位置
    function logParameter(target: Object, propertyName: string, index: number) {
      console.log(target);
      console.log(propertyName);
      console.log(index);
    }
    
    class Employee {
      greet(@logParameter message: string): string {
        return `hello ${message}`;
      }
    }
    const emp = new Employee();
    emp.greet('hello');
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 访问器

    使用起来方式与方法装饰一致,如下:

    function modification(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
      console.log(target);
      console.log("prop " + propertyKey);
      console.log("desc " + JSON.stringify(descriptor) + "\n\n");
    };
    
    class Person{
    _name: string;
    constructor() {
      this._name = 'huihui';
    }
    
    @modification
    get name() {
      return this._name
    }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

# 装饰器工厂

如果想要传递参数,使装饰器变成类似工厂函数,只需要在装饰器函数内部再函数一个函数即可,如下:

function addAge(age: number) {
  return function(constructor: Function) {
    constructor.prototype.age = age
  }
}

@addAge(10)
class Person{
  name: string;
  age!: number;
  constructor() {
    this.name = 'huihui';
  }
}

let person = new Person();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

执行顺序:当多个装饰器应用于一个声明上,将由上至下依次对装饰器表达式求值,求值的结果会被当作函数,由下至上依次调用。

function f() {
  console.log("f(): evaluated");
  return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("f(): called");
  }
}

function g() {
  console.log("g(): evaluated");
  return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("g(): called");
  }
}

class C {
  @f()
  @g()
  method() {}
}

// 输出
// f(): evaluated
// g(): evaluated
// g(): called
// f(): called
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
最近更新: 2023年03月21日 14:47:21