Pengzna's blog 👋

Jul 25, 2022

TypeScript ·入门

参考资料:https://juejin.cn/post/6844904182843965453

img

1. TypeScript 简介

1.1 TypeScript 与 JavaScript 的区别

TypeScript JavaScript
JavaScript 的超集用于解决大型项目的代码复杂性 一种脚本语言,用于创建动态网页。
可以在编译期间发现并纠正错误 作为一种解释型语言,只能在运行时发现错误
强类型,支持静态和动态类型 弱类型,没有静态类型选项
最终被编译成 JavaScript 代码,使浏览器可以理解 可以直接在浏览器中使用
支持模块、泛型和接口 不支持模块,泛型或接口
支持 ES3,ES4,ES5 和 ES6 等 不支持编译其他 ES3,ES4,ES5 或 ES6 功能
社区的支持仍在增长,而且还不是很大 大量的社区支持以及大量文档和解决问题的支持

1.2 获取 TypeScript

命令行的 TypeScript 编译器可以使用 Node.js 包来安装。

1.安装 TypeScript

$ npm install -g typescript
复制代码

2.编译 TypeScript 文件

$ tsc helloworld.ts
# helloworld.ts => helloworld.js

2. TypeScript 基础类型

2.1 Boolean 类型

let isDone: boolean = false;
// ES5:var isDone = false;

2.2 Number 类型

let count: number = 10;
// ES5:var count = 10;

2.3 String 类型

let name: string = "Semliker";
// ES5:var name = 'Semlinker';

2.4 Array 类型

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

let list: Array<number> = [1, 2, 3]; // Array<number>泛型语法
// ES5:var list = [1,2,3];

2.5 Enum 类型

使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript 支持数字的和基于字符串的枚举。

2.5.1.数字枚举

enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}

let dir: Direction = Direction.NORTH;

默认情况下,NORTH 的初始值为 0,其余的成员会从 1 开始自动增长。换句话说,Direction.SOUTH 的值为 1,Direction.EAST 的值为 2,Direction.WEST 的值为 3。上面的枚举示例代码经过编译后会生成以下代码:

'use strict'
var Direction
;(function (Direction) {
Direction[(Direction['NORTH'] = 0)] = 'NORTH'
Direction[(Direction['SOUTH'] = 1)] = 'SOUTH'
Direction[(Direction['EAST'] = 2)] = 'EAST'
Direction[(Direction['WEST'] = 3)] = 'WEST'
})(Direction || (Direction = {}))
var dir = Direction.NORTH

当然我们也可以设置 NORTH 的初始值,比如:

enum Direction {
NORTH = 3,
SOUTH,
EAST,
WEST,
}

2.5.2.字符串枚举

在 TypeScript 2.4 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

enum Direction {
NORTH = "NORTH",
SOUTH = "SOUTH",
EAST = "EAST",
WEST = "WEST",
}

以上代码对于的 ES5 代码如下:

'use strict'
var Direction
;(function (Direction) {
Direction['NORTH'] = 'NORTH'
Direction['SOUTH'] = 'SOUTH'
Direction['EAST'] = 'EAST'
Direction['WEST'] = 'WEST'
})(Direction || (Direction = {}))
复制代码

2.5.3.异构枚举

异构枚举的成员值是数字和字符串的混合:

enum Enum {
A,
B,
C = "C",
D = "D",
E = 8,
F,
}

以上代码对于的 ES5 代码如下:

'use strict'
var Enum
;(function (Enum) {
Enum[(Enum['A'] = 0)] = 'A'
Enum[(Enum['B'] = 1)] = 'B'
Enum['C'] = 'C'
Enum['D'] = 'D'
Enum[(Enum['E'] = 8)] = 'E'
Enum[(Enum['F'] = 9)] = 'F'
})(Enum || (Enum = {}))

通过观察上述生成的 ES5 代码,我们可以发现数字枚举相对字符串枚举多了 “反向映射”:

console.log(Enum.A) //输出:0
console.log(Enum[0]) // 输出:A

2.6 Any 类型

在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)。

let notSure: any = 666;
notSure = "Semlinker";
notSure = false;

any 类型本质上是类型系统的一个逃逸舱。作为开发者,这给了我们很大的自由:TypeScript 允许我们对 any 类型的值执行任何操作,而无需事先执行任何形式的检查。比如:

let value: any;

value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK
复制代码

在许多场景下,这太宽松了。使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。

2.7 Unknown 类型

就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)。下面我们来看一下 unknown 类型的使用示例:

let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK

value 变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown 的值赋值给其他类型的变量时会发生什么?

let value: unknown;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值。毕竟我们不知道变量 value 中存储了什么类型的值。

现在让我们看看当我们尝试对类型为 unknown 的值执行操作时会发生什么。以下是我们在之前 any 章节看过的相同操作:

let value: unknown;

value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error

value 变量类型设置为 unknown 后,这些操作都不再被认为是类型正确的。通过将 any 类型改变为 unknown 类型,我们已将允许所有更改的默认设置,更改为禁止任何更改

2.8 Tuple 类型

众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。

元组可用于定义具有有限数量的未命名属性的类型。每个属性都有一个关联的类型。使用元组时,必须提供每个属性的值。为了更直观地理解元组的概念,我们来看一个具体的例子:

let tupleType: [string, boolean];
tupleType = ["Semlinker", true];

在上面代码中,我们定义了一个名为 tupleType 的变量,它的类型是一个类型数组 [string, boolean],然后我们按照正确的类型依次初始化 tupleType 变量。与数组一样,我们可以通过下标来访问元组中的元素:

console.log(tupleType[0]); // Semlinker
console.log(tupleType[1]); // true

在元组初始化的时候,如果出现类型不匹配的话,比如:

tupleType = [true, "Semlinker"];

此时,TypeScript 编译器会提示以下错误信息:

[0]: Type 'true' is not assignable to type 'string'.
[1]: Type 'string' is not assignable to type 'boolean'.

很明显是因为类型不匹配导致的。在元组初始化的时候,我们还必须提供每个属性的值,不然也会出现错误,比如:

tupleType = ["Semlinker"];

此时,TypeScript 编译器会提示以下错误信息:

Property '1' is missing in type '[string]' but required in type '[string, boolean]'.

2.9 Void 类型

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

// 声明函数返回值为void
function warnUser(): void {
console.log('This is my warning message')
}

以上代码编译生成的 ES5 代码如下:

'use strict'
function warnUser() {
console.log('This is my warning message')
}

需要注意的是,声明一个 void 类型的变量没有什么作用,因为它的值只能为 undefinednull

let unusable: void = undefined;

2.10 Null 和 Undefined 类型

TypeScript 里,undefinednull 两者有各自的类型分别为 undefinednull

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

默认情况下 nullundefined 是所有类型的子类型。 就是说你可以把 nullundefined 赋值给 number 类型的变量。然而,如果你指定了--strictNullChecks 标记,nullundefined 只能赋值给 void 和它们各自的类型。

2.11 Never 类型

never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

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

function infiniteLoop(): never {
while (true) {}
}

在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:

type Foo = string | number

function controlFlowAnalysisWithNever(foo: Foo) {
if (typeof foo === 'string') {
// 这里 foo 被收窄为 string 类型
} else if (typeof foo === 'number') {
// 这里 foo 被收窄为 number 类型
} else {
// foo 在这里是 never
const check: never = foo
}
}

注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:

type Foo = string | number | boolean
复制代码

然而他忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保

controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

3. TypeScript 断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。

类型断言有两种形式:

3.1. “尖括号” 语法

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

3.2. as 语法

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

4. 类型守卫

A type guard is some expression that performs a runtime check that guarantees the type in some scope. —— TypeScript 官方文档

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护:

4.1 in 关键字

interface Admin {
name: string
privileges: string[]
}

interface Employee {
name: string
startDate: Date
}

type UnknownEmployee = Employee | Admin

function printEmployeeInformation(emp: UnknownEmployee) {
console.log('Name: ' + emp.name)
if ('privileges' in emp) {
console.log('Privileges: ' + emp.privileges)
}
if ('startDate' in emp) {
console.log('Start Date: ' + emp.startDate)
}
}

4.2 typeof 关键字

function padLeft(value: string, padding: string | number) {
if (typeof padding === 'number') {
return Array(padding + 1).join(' ') + value
}
if (typeof padding === 'string') {
return padding + value
}
throw new Error(`Expected string or number, got '${padding}'.`)
}

typeof 类型保护只支持两种形式:typeof v === "typename"typeof v !== typename"typename" 必须是 "number""string""boolean""symbol"。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

4.3 instanceof 关键字

interface Padder {
getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) {}
getPaddingString() {
return Array(this.numSpaces + 1).join(' ')
}
}

class StringPadder implements Padder {
constructor(private value: string) {}
getPaddingString() {
return this.value
}
}

let padder: Padder = new SpaceRepeatingPadder(6)

if (padder instanceof SpaceRepeatingPadder) {
// padder的类型收窄为 'SpaceRepeatingPadder'
}

4.4 自定义类型保护的类型谓词

function isNumber(x: any): x is number {
return typeof x === 'number'
}

function isString(x: any): x is string {
return typeof x === 'string'
}

5. 联合类型和类型别名

5.1 联合类型

联合类型通常与 nullundefined 一起使用:

const sayHello = (name: string | undefined) => {
/* ... */
}

例如,这里 name 的类型是 string | undefined 意味着可以将 stringundefined 的值传递给sayHello 函数。

sayHello("Semlinker");
sayHello(undefined);

通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。

5.2 可辨识联合

TypeScript 可辨识联合(Discriminated Unions)类型,也称为代数数据类型或标签联合类型。它包含 3 个要点:可辨识、联合类型和类型守卫。

这种类型的本质是结合联合类型和字面量类型的一种类型保护方法。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。

1.可辨识

可辨识要求联合类型中的每个元素都含有一个单例类型属性,比如:

enum CarTransmission {
Automatic = 200,
Manual = 300
}

interface Motorcycle {
vType: "motorcycle"; // discriminant
make: number; // year
}

interface Car {
vType: "car"; // discriminant
transmission: CarTransmission
}

interface Truck {
vType: "truck"; // discriminant
capacity: number; // in tons
}

在上述代码中,我们分别定义了 MotorcycleCarTruck 三个接口,在这些接口中都包含一个 vType 属性,该属性被称为可辨识的属性,而其它的属性只跟特性的接口相关。

2.联合类型

基于前面定义了三个接口,我们可以创建一个 Vehicle 联合类型:

type Vehicle = Motorcycle | Car | Truck;

现在我们就可以开始使用 Vehicle 联合类型,对于 Vehicle 类型的变量,它可以表示不同类型的车辆。

3.类型守卫

下面我们来定义一个 evaluatePrice 方法,该方法用于根据车辆的类型、容量和评估因子来计算价格,具体实现如下:

const EVALUATION_FACTOR = Math.PI;
function evaluatePrice(vehicle: Vehicle) {
return vehicle.capacity * EVALUATION_FACTOR;
}

const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);

对于以上代码,TypeScript 编译器将会提示以下错误信息:

Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.

原因是在 Motorcycle 接口中,并不存在 capacity 属性,而对于 Car 接口来说,它也不存在 capacity 属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使用类型守卫。下面我们来重构一下前面定义的 evaluatePrice 方法,重构后的代码如下:

function evaluatePrice(vehicle: Vehicle) {
switch (vehicle.vType) {
case 'car':
return vehicle.transmission * EVALUATION_FACTOR
case 'truck':
return vehicle.capacity * EVALUATION_FACTOR
case 'motorcycle':
return vehicle.make * EVALUATION_FACTOR
}
}

在以上代码中,我们使用 switchcase 运算符来实现类型守卫,从而确保在 evaluatePrice 方法中,我们可以安全地访问 vehicle 对象中的所包含的属性,来正确的计算该车辆类型所对应的价格。

5.3 类型别名

类型别名用来给一个类型起个新名字。

type Message = string | string[];

let greet = (message: Message) => {
// ...
};

6. 交叉类型

TypeScript 交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性

interface IPerson {
id: string
age: number
}

interface IWorker {
companyId: string
}

type IStaff = IPerson & IWorker

const staff: IStaff = {
id: 'E1006',
age: 33,
companyId: 'EFT',
}

console.dir(staff)

在上面示例中,我们首先为 IPerson 和 IWorker 类型定义了不同的成员,然后通过 & 运算符定义了 IStaff 交叉类型,所以该类型同时拥有 IPerson 和 IWorker 这两种类型的成员。

7. TypeScript 函数

7.1 TypeScript 函数与 JavaScript 函数的区别

TypeScript JavaScript
含有类型 无类型
箭头函数 箭头函数(ES2015)
函数类型 无函数类型
必填和可选参数 所有参数都是可选的
默认参数 默认参数
剩余参数 剩余参数
函数重载 无函数重载

7.2 箭头函数

1.常见语法

myBooks.forEach(() => console.log('reading'));

myBooks.forEach(title => console.log(title));

myBooks.forEach((title, idx, arr) =>
console.log(idx + '-' + title);
);

myBooks.forEach((title, idx, arr) => {
console.log(idx + '-' + title);
});

2.使用示例

// 未使用箭头函数
function Book() {
let self = this
self.publishDate = 2016
setInterval(function () {
console.log(self.publishDate)
}, 1000)
}

// 使用箭头函数
function Book() {
this.publishDate = 2016
setInterval(() => {
console.log(this.publishDate)
}, 1000)
}

7.3 参数类型和返回类型

function createUserId(name: string, id: number): string {
return name + id
}

7.4 函数类型

// 声明函数类型,然后再赋值
let IdGenerator: (chars: string, nums: number) => string

function createUserId(name: string, id: number): string {
return name + id
}

IdGenerator = createUserId

7.5 可选参数及默认参数

// 可选参数:加个问号即可
function createUserId(name: string, id: number, age?: number): string {
return name + id
}

// 默认参数
function createUserId(
name: string = 'Semlinker',
id: number,
age?: number
): string {
return name + id
}

在声明函数时,可以通过 ? 号来定义可选参数,比如 age?: number 这种形式。在实际使用时,需要注意的是可选参数要放在普通参数的后面,不然会导致编译错误。

7.6 剩余参数

// 加...表示剩余参数
function push(array, ...items) {
items.forEach(function (item) {
array.push(item);
});
}

let a = [];
push(a, 1, 2, 3);

7.7 函数重载

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。要解决前面遇到的问题,方法就是为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。

function add(a: number, b: number): number
function add(a: string, b: string): string
function add(a: string, b: number): string
function add(a: number, b: string): string
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString()
}
return a + b
}

在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。之后,可恶的错误消息又消失了,因为这时 result 变量的类型是 string 类型。在 TypeScript 中除了可以重载普通函数之外,我们还可以重载类中的成员方法。

方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:

class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: string, b: number): string;
add(a: number, b: string): string;
add(a: Combinable, b: Combinable) {
if (typeof a === "string" || typeof b === "string") {
return a.toString() + b.toString();
}
return a + b;
}
}

const calculator = new Calculator();
const result = calculator.add("Semlinker", " Kakuqo");

这里需要注意的是,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator 类中,add(a: Combinable, b: Combinable){ } 并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。

8. TypeScript 数组

8.1 数组解构

let x: number
let y: number
let z: number
let five_array = [0, 1, 2, 3, 4]
;[x, y, z] = five_array

8.2 数组展开运算符

let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];

8.3 数组遍历

let colors: string[] = ['red', 'green', 'blue']
for (let i of colors) {
console.log(i)
}

9. TypeScript 对象

9.1 对象解构

let person = {
name: "Semlinker",
gender: "Male",
};

let { name, gender } = person;

9.2 对象展开运算符

let person = {
name: "Semlinker",
gender: "Male",
address: "Xiamen",
};

// 组装对象
let personWithAge = { ...person, age: 33 };

// 获取除了某些项外的其它项
let { name, ...rest } = person;

10. TypeScript 接口

在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。

TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

10.1 对象的形状

interface Person {
name: string;
age: number;
}

let Semlinker: Person = {
name: "Semlinker",
age: 33,
};

10.2 可选 | 只读属性

interface Person {
readonly name: string
age?: number
}

只读属性用于限制只能在对象刚刚创建的时候修改其值。此外 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!

11. TypeScript 类

11.1 类的属性与方法

在面向对象语言中,类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。

在 TypeScript 中,我们可以通过 Class 关键字来定义一个类:

class Greeter {
// 静态属性
static cname: string = 'Greeter'
// 成员属性
greeting: string

// 构造函数 - 执行初始化操作
constructor(message: string) {
this.greeting = message
}

// 静态方法
static getClassName() {
return 'Class name is Greeter'
}

// 成员方法
greet() {
return 'Hello, ' + this.greeting
}
}

let greeter = new Greeter('world')

那么成员属性与静态属性,成员方法与静态方法有什么区别呢?这里无需过多解释,我们直接看一下以下编译生成的 ES5 代码:

'use strict'
var Greeter = /** @class */ (function () {
// 构造函数 - 执行初始化操作
function Greeter(message) {
this.greeting = message
}
// 静态方法
Greeter.getClassName = function () {
return 'Class name is Greeter'
}
// 成员方法
Greeter.prototype.greet = function () {
return 'Hello, ' + this.greeting
}
// 静态属性
Greeter.cname = 'Greeter'
return Greeter
})()
var greeter = new Greeter('world')
复制代码

11.2 访问器

在 TypeScript 中,我们可以通过 gettersetter 方法来实现数据的封装和有效性校验,防止出现异常数据。

let passcode = 'Hello TypeScript'

class Employee {
private _fullName: string

get fullName(): string {
return this._fullName
}

set fullName(newName: string) {
if (passcode && passcode == 'Hello TypeScript') {
this._fullName = newName
} else {
console.log('Error: Unauthorized update of employee!')
}
}
}

let employee = new Employee()
employee.fullName = 'Semlinker'
if (employee.fullName) {
console.log(employee.fullName)
}

11.3 类的继承

继承 (Inheritance) 是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系。

继承是一种 is-a 关系:

img

在 TypeScript 中,我们可以通过 extends 关键字来实现继承:

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)
}
}

let sam = new Snake('Sammy the Python')
sam.move()

11.4 ECMAScript 私有字段

在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段,使用方式如下:

class Person {
#name: string

constructor(name: string) {
this.#name = name
}

greet() {
console.log(`Hello, my name is ${this.#name}!`)
}
}

let semlinker = new Person('Semlinker')

semlinker.#name
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

与常规属性(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:

  • 私有字段以 # 字符开头,有时我们称之为私有名称;
  • 每个私有字段名称都唯一地限定于其包含的类;
  • 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
  • 私有字段不能在包含的类之外访问,甚至不能被检测到。

12. TypeScript 泛型

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

12.1 泛型接口

interface GenericIdentityFn<T> {
(arg: T): T;
}

12.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;
};

12.3 泛型变量

对刚接触 TypeScript 泛型的小伙伴来说,看到 T 和 E,还有 K 和 V 这些泛型变量时,估计会一脸懵逼。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。也就是说使用大写字母 A-Z 定义的类型变量都属于泛型,把 T 换成 A,也是一样的。下面我们介绍一下一些常见泛型变量代表的意思:

  • T(Type):表示一个 TypeScript 类型
  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型

12.4 泛型工具类型

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考虑,这里我们只简单介绍 Partial 工具类型。不过在具体介绍之前,我们得先介绍一些相关的基础知识,方便读者自行学习其它的工具类型。

1.typeof

在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型。

interface Person {
name: string
age: number
}

const sem: Person = { name: 'semlinker', age: 30 }
type Sem = typeof sem // -> Person

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

type Func = typeof toArray // -> (x: number) => number[]

2.keyof

keyof 操作符可以用来一个对象中的所有 key 值:

interface Person {
name: string;
age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number

3.in

in 用来遍历枚举类型:

type Keys = "a" | "b" | "c"

type Obj = {
[p in Keys]: any
} // -> { a: any, b: any, c: any }

4.infer

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

type ReturnType<T> = T extends (
...args: any[]
) => infer R ? R : any;

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

5.extends

有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。

interface ILengthwise {
length: number;
}

function loggingIdentity<T extends ILengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // Error, number doesn't have a .length property

这时我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3});

6.Partial

Partial<T> 的作用就是将某个类型里的属性全部变为可选项 ?

定义:

/**
* node_modules/typescript/lib/lib.es5.d.ts
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};

在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。

示例:

interface Todo {
title: string;
description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
title: "organize desk",
description: "clear clutter",
};

const todo2 = updateTodo(todo1, {
description: "throw out trash",
});

在上面的 updateTodo 方法中,我们利用 Partial<T> 工具类型,定义 fieldsToUpdate 的类型为 Partial<Todo>,即:

{
title?: string | undefined;
description?: string | undefined;
}
OLDER > < NEWER