{"action":"closed","issue":{"url":"https://api.github.com/repos/deqwin/blog/issues/4","repository_url":"https://api.github.com/repos/deqwin/blog","labels_url":"https://api.github.com/repos/deqwin/blog/issues/4/labels{/name}","comments_url":"https://api.github.com/repos/deqwin/blog/issues/4/comments","events_url":"https://api.github.com/repos/deqwin/blog/issues/4/events","html_url":"https://github.com/deqwin/blog/issues/4","id":493707790,"node_id":"MDU6SXNzdWU0OTM3MDc3OTA=","number":4,"title":"深入 Typescript 类型系统","user":{"login":"deqwin","id":18345478,"node_id":"MDQ6VXNlcjE4MzQ1NDc4","avatar_url":"https://avatars3.githubusercontent.com/u/18345478?v=4","gravatar_id":"","url":"https://api.github.com/users/deqwin","html_url":"https://github.com/deqwin","followers_url":"https://api.github.com/users/deqwin/followers","following_url":"https://api.github.com/users/deqwin/following{/other_user}","gists_url":"https://api.github.com/users/deqwin/gists{/gist_id}","starred_url":"https://api.github.com/users/deqwin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/deqwin/subscriptions","organizations_url":"https://api.github.com/users/deqwin/orgs","repos_url":"https://api.github.com/users/deqwin/repos","events_url":"https://api.github.com/users/deqwin/events{/privacy}","received_events_url":"https://api.github.com/users/deqwin/received_events","type":"User","site_admin":false},"labels":[],"state":"closed","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":0,"created_at":"2019-09-15T07:58:17Z","updated_at":"2019-11-05T00:32:57Z","closed_at":"2019-11-05T00:32:57Z","author_association":"OWNER","body":"最近项目中准备推广接入 Typescript，抽空复习了一波相关的技术知识。说复习是因为之前看过了然而现在已经忘得一干二净了，除了不常用的原因外，也是因为 Typescript 知识相对比较零散，学习时难成体系，所以趁这个机会整理整理，就当学习笔记吧。\r\n\r\n## 前言\r\n\r\nTypescript （简称 TS）是 Javascript 的一个超集，它提供一套全面的对语法定义、使用的约束，来强化 JS 代码的可读性和可维护性，其中最重要的就是 TS 的类型系统。TS 类型系统提供了一套完备的声明和使用类型的方案，灵活但也复杂难记，导致 TS 的学习成本较高。\r\n\r\n学习 TS 的类型系统，首先要记住很重要的一点：类型系统只在编译时起作用，最终必然不会出现在你的业务代码里。认识到这一点是很必要的，因为我们学习 TS 很容易将它跟 JS 的语法代码放在一块来理解，其实这是不对的， TS 类型系统仅仅就是 TS 对 JS 的一种强化手段，学习 TS 类型系统，首先就是要忘记 JS，要把类型系统跟 JS 尽可能划分开来，才能避免一些语法的理解干扰。\r\n\r\n让类型更加灵活通用，是类型系统中一切语法特性的目标。独立地理解 TS 类型系统，也是基于这个目标由浅到深地了解。\r\n\r\n## 从最简单的类型讲起\r\n\r\n声明类型是使用类型的前提，TS 提供了许多用于声明类型的语法特性，其唯一目的就是不断完善和强化 TS 声明类型的能力，让类型声明更加地灵活、可复用。\r\n\r\n我们从最简单的类型开始来认识 TS，包括基础类型和高级类型。在 TS 中要声明一个类型，最简单的就是使用 `type` 关键字。\r\n\r\n### 基础类型\r\n\r\n基础类型对应了 JS 的多个基础的数据类型，主要包括：\r\n\r\n- 原始类型：`boolean`、`number`、`string`、`void`、`any`、`null`、`undefined` 以及 `symbol`。\r\n- 引用类型：对象 `Object` 、函数 `Function` 以及数组 `Array` 等。\r\n\r\n这是 TS 中最简单的数据类型了，我们列举几个经常用到的：\r\n\r\n```ts\r\n// 声明的时候：\r\ntype _string = string;  // 字符串类型\r\ntype _number = number;  // 数字类型\r\ntype _boolean = boolean;  // 布尔值类型\r\ntype _any = any;  // 任意值\r\ntype _object = {  // 对象类型\r\n  name: string;\r\n  age: number\r\n}\r\ntype _array = _object[];  // 数组类型\r\ntype _function = (user: _object) => number;  // 函数类型\r\n\r\n// 使用的时候：\r\nconst str: _string = '111';\r\nconst num: _number = 3;\r\nconst show: _boolean = true;\r\nlet err: _any = -1;\r\nerr = 'something wrong';\r\nconst user: _object = {\r\n  name: 'hankle',\r\n  age: 23\r\n}\r\nconst list: _array = [user];\r\nconst getAge: _function = (user) => {\r\n  return user.age\r\n}\r\n```\r\n\r\n我们声明了几个不同的类型，并在使用的时候以 `: [type]` 的方式接入，进而对我们的变量做了一层类型约束。从上边例子我们也可以看出，`type` 可以给已有类型声明一个别名。\r\n\r\n### 高级类型\r\n\r\n高级类型，可以让我们更加灵活地重用已有的类型，是复用的一种有效手段。\r\n\r\n#### 联合类型\r\n\r\n联合类型表示取值可以为多种类型中的其中一种，使用 `|` 连接符，作用机制类似于 \"或\"。\r\n\r\n```ts\r\ntype keyType = string | number;\r\n\r\nconst strKey: keyType = 'key';\r\nconst numKey: keyType = 1;\r\n```\r\n\r\n键类型可以是 string，也可以是 number。\r\n\r\n#### 交叉类型\r\n\r\n交叉类型表示变量应该具备多个类型的所有特性，使用 `&` 连接符，作用机制类似于 \"并\"，多用于对象类型。在我们需要对一些已定义好的 base 对象类型进行扩展时，可以使用交叉类型来声明。\r\n\r\n```ts\r\ntype User1 = {\r\n  name: string;\r\n  age: number\r\n};\r\ntype User2 = {\r\n  sex: number\r\n}\r\n\r\nconst user: User1 & User2 = {\r\n  name: 'hankle',\r\n  age: 23,\r\n  sex: 0\r\n};\r\n```\r\n\r\n`_User1 & _User2` 表示变量需要具备 _User1 和 _User2 中的所有属性。\r\n\r\n#### 字面量类型\r\n\r\n`type` 声明的类型还可以是一个字面量，表示这个类型的取值只能是这个固定值，这就是字面量类型：\r\n\r\n```ts\r\ntype coco = 'coco';\r\nconst str: coco = 'coco coco';\r\n// 不能将类型“\"coco coco\"”分配给类型“\"coco\"”。\r\n```\r\n\r\n有一种常用的字面量类型，叫「字符串字面量类型」，它结合联合类型进行使用，用来约束取值只能是某几个字符串中的一个：\r\n\r\n```ts\r\ntype Key = 'name' | 'age';\r\ntype User = {\r\n  name: string,\r\n  age: number\r\n}\r\n\r\nconst user: User = { name: 'hankle', age: 23 }\r\nconst getVal = (user: User, key: Key): any => user[key]\r\n\r\ngetVal(user, 'sex');\r\n// 类型“\"sex\"”的参数不能赋给类型“Key”的参数。\r\n```\r\nkey 类型限制了对于 user 对象可取的属性键值，避免代码企图获取 user 中并不存在的属性。\r\n\r\n\r\n## 接口：声明类型的另一种方式\r\n\r\n除了 `type`，在 TS 中还有另外一种声明类型的方式，那就是接口（Interface）。使用 `interface` 关键字：\r\n\r\n```ts\r\n/**\r\n * 声明对象\r\n * 等同于：\r\n * type User = { name: string; age: number }\r\n */\r\ninterface User {\r\n  name: string;\r\n  age: number\r\n}\r\n\r\n/**\r\n * 声明数组\r\n * 等同于：\r\n * type List = User[]\r\n */\r\ninterface List {\r\n  [index: number]: User\r\n}\r\n\r\n/**\r\n * 声明函数\r\n * 等同于：\r\n * type findFunc = (list: List, name: string) => User\r\n */\r\ninterface findFunc {\r\n  (list: List, name: string): User\r\n}\r\n```\r\n\r\n为什么已经有了 `type`，还需要另外创造出「接口」来声明类型呢？事实上在很多 OOP 语言（比如 Java）中，接口是一个很重要的概念，用于对类（class）行为进行抽象。因此，TS 的接口除了描述对象类型、数组类型或函数类型的形状外，还可以描述类的形状。通过实现（implements）和继承（extends），接口定义的类型可以实现高度复用，这本就是 TS 类型系统的初衷。\r\n\r\n在 TS 中，类（class）类型接口的声明和实现是这样的：\r\n\r\n```ts\r\n// 定义了一个报警类接口，要求实现该接口的类都必须拥有 alert 方法\r\ninterface Alarm {\r\n    alert();\r\n}\r\n// SecurityDoor 类实现了该接口，并实现了 alert 方法\r\nclass SecurityDoor implements Alarm {\r\n    alert() {\r\n        console.log('SecurityDoor alert');\r\n    }\r\n}\r\n// Car 类没有实现 alert 方法，报错了\r\nclass Car implements Alarm {\r\n}\r\n// 类“Car”错误实现接口“Alarm”。\r\n// Property 'alert' is missing in type 'Car' but required in type 'Alarm'.\r\n```\r\n\r\n`type` 关键字声明的类型可以通过联合类型对类型进行扩展，而接口则是利用继承（extends）来实现的：\r\n\r\n```ts\r\ninterface User1 {\r\n  name: string;\r\n  age: number\r\n};\r\n// 声明了接口 User2，继承自接口 User1，它需要包含 User1 的形状\r\ninterface User2 extends User1 {\r\n  sex: number\r\n}\r\n\r\nconst userq: User2 = {\r\n  name: 'hankle',\r\n  sex: 0\r\n};\r\n// Property 'age' is missing in type '{ name: string; sex: number; }' but required in type 'User2'.\r\n```\r\n\r\n至于说什么时候使用 `type`，什么时候使用 `interface` ，网上倒是有不少关于这个问题的解读。其实这两个能做的事情都大同小异，至于选用哪个，我认为在平时业务场景开发中，如果多是对一些函数结构、象结构、类结构作类型约束的话，使用 `interface` 会更加地语义化，建议多用 `interface` 来声明类型。\r\n\r\n## 泛型：让你的类型更通用\r\n\r\n如果现有一个 `find` 函数，作用等同于 `Array.find`，需要我们自己声明它的函数类型，那到目前来说，我们可能只能利用函数的声明重载来这样写：\r\n\r\n```ts\r\n// 声明重载\r\nfunction find(list: string[], func: (item: string) => boolean): string | null;\r\nfunction find(list: number[], func: (item: number) => boolean): number | null;\r\nfunction find(list: object[], func: (item: object) => boolean): object | null;\r\n// 实现\r\nfunction find(list, func) {\r\n  return list.find(func)\r\n}\r\n```\r\n\r\n函数重载允许通过多次声明函数的不同形状来实现函数的联合类型。`find` 方法允许传入多种类型的数组，其返回值也将由传入数组所包含的元素类型决定。从上边可以看出，我们重复声明的只是不同的元素类型，函数的参结构是没有变化的，所以出现了很多重复代码。\r\n\r\n### 函数的泛型\r\n\r\n泛型（Generics）正是为了解决这个问题。顾名思义，泛型代表不确定的类型，它允许复杂类型在声明的时候内部保留不确定的类型，等到使用的时候再具体指定。泛型相当于一个函数，具体说是类型的工厂函数，既然是函数那就是要“传参”的，泛型传参使用`<>`实现。上边例子用泛型实现是这样的：\r\n\r\n```ts\r\n// find 的类型是一个泛型\r\nfunction find<T>(list: T[], func: (item: T) => boolean): T | null {\r\n  return list.find(func)\r\n}\r\n\r\n// 使用时再指定具体类型\r\nfind<number>([1,2,3], item => item > 2)\r\n```\r\n\r\n当然，你也可以用接口来声明函数类型：\r\n\r\n```ts\r\n// find 的类型 FindType 是一个泛型\r\ninterface FindType {\r\n  <T>(list: T[], func: (item: T) => boolean): T | null\r\n}\r\nconst find: FindType = function (list, func) {\r\n  return list.find(func)\r\n}\r\n\r\n// 使用时指定具体类型\r\nfind<number>([1,2,3], item => item > 2)\r\n```\r\n\r\n泛型还可以允许指定多个类型参数：\r\n\r\n```ts\r\n// tuple元素互换\r\nfunction swap<T, U>(tuple: [T, U]): [U, T] {\r\n  return [tuple[1], tuple[0]];\r\n}\r\n\r\nswap<number, string>([7, 'seven']); // ['seven', 7]\r\n```\r\n\r\n### 类的泛型\r\n\r\n泛型不仅可以作用在函数类型上，还普遍地应用在类的类型定义中。和函数调用时再具体指定类型相似，类的泛型是在实例化对象时才具体指定的：\r\n\r\n```ts\r\n// 声明了一个泛型类\r\nclass EleList<T> {\r\n  list: T[]\r\n  add: (item: T) => number\r\n}\r\n\r\n// 实例化时，指定为字符串类型\r\nconst users = new EleList<string>();\r\nusers.list = ['hankle', 'nancy'];\r\nusers.add = function (item) {\r\n  this.list.push(item);\r\n  return this.list.length;\r\n}\r\n```\r\n\r\n### 泛型约束\r\n\r\n泛型允许我们预设和使用一个不确定的类型，但有时候“不确定”也对我们的使用造成了影响，比如我们有这样一个用于获取长度的工具方法，入参可以是一个数组，也可以是一个类数组对象：\r\n\r\n```ts\r\nfunction get<T>(arg: T): number {\r\n  return arg.length; // 类型“T”上不存在属性“length”。\r\n}\r\n```\r\n\r\n然而我们发现它报错了。类型 T 在这里是任意类型，我们并不能保证使用时指定的类型都存在属性 `length`。我们希望T是有限制的，它只能是数组或类数组对象，或者更具体地说，它必须包含 `length` 属性。所以这时候，泛型约束派上用场了：\r\n\r\n```ts\r\n// 声明 LengthType 类型，必须包含 length 属性\r\ninterface LengthType {\r\n  length: number;\r\n}\r\n\r\n// T 继承自 LengthType 类型，也就表明它必须具有 length 属性\r\nfunction get<T extends LengthType>(arg: T): number {\r\n  return arg.length;\r\n}\r\n```\r\n\r\n这里我们声明了一个具有 `length` 属性的类型，然后让类型 T 继承该类型，从而对泛型的结构进行约束。我们说过接口继承也使用了 `extends` 关键字，它的深层含义就是继承者必须包含被继承者的所有特性，所以泛型约束同样也是借助 `extends` 的这个作用来限制泛型的结构形状。泛型约束可以帮助我们减少许多无意义的判断逻辑，在实际开发过程中，我们的泛型往往都不是完全的任意类型，因此应当善于使用泛型约束。\r\n\r\n## 高阶类型：泛型还能这么玩\r\n\r\n泛型可以是 TS 类型系统最值得利用的特性了。前边说过，泛型是一个类型的工厂函数，换句话说，利用泛型可以进一步构建出各式各样的类型。TS 官方把这类类型同样称为高级类型，而我认为把它们叫做高阶类型，更为合适。\r\n\r\n### 映射类型\r\n\r\n映射类型就是一种常用的泛型结构。映射类型能对一个旧类型的各项属性进行映射，进而生成一个新类型。TS 标准库已经内置了多个十分实用的映射类型，包括 `Partial`，`Required`，`Readonly`，`Pick` 和 `Record`等，我们挑选几个解释一下。\r\n\r\n#### Partial\\<T\\>\r\n\r\n```ts\r\ntype Partial<T> = {\r\n    [P in keyof T]?: T[P];\r\n};\r\n```\r\n\r\n`keyof` 返回的是 T 中所有属性键名组成的字符串字面量类型，所以 `Partial` 用于把传入类型 T 中的各项属性转化为可选属性。\r\n\r\n```ts\r\ninterface User {\r\n  name: string;\r\n  age: number;\r\n  sex: number;\r\n};\r\n\r\n// User 类型的属性是必选的，PartialUser 类型的属性是可选的\r\ntype PartialUser = Partial<User>\r\n/**\r\n * type PartialUser = {\r\n *   name?: string;\r\n *   age?: number;\r\n *   sex?: number;\r\n * }\r\n */\r\n```\r\n\r\n#### Required\\<T\\>\r\n\r\n```ts\r\ntype Required<T> = {\r\n    [P in keyof T]-?: T[P];\r\n};\r\n```\r\n\r\n和 `Partial` 相反，`Required` 用于把传入类型 T 中的各项属性转化为必选属性。\r\n\r\n```ts\r\ninterface User {\r\n  name?: string;\r\n  age?: number;\r\n  sex?: number;\r\n};\r\n\r\n// User 类型的属性是可选的，RequiredUser 类型的属性是必选的\r\ntype RequiredUser = Required<User>\r\n/**\r\n * type RequiredUser = {\r\n *   name: string;\r\n *   age: number;\r\n *   sex: number;\r\n * }\r\n */\r\n```\r\n\r\n#### Readonly\\<T\\>\r\n\r\n```ts\r\ntype Readonly<T> = {\r\n    readonly [P in keyof T]: T[P];\r\n};\r\n```\r\n\r\n`Readonly` 用于将传入类型 T 中的所有属性转化为只读属性。\r\n\r\n```ts\r\ninterface User {\r\n  name: string;\r\n  age: number;\r\n  sex: number;\r\n};\r\n\r\n// User 类型的属性是可写的，ReadonlyUser 类型的属性是只读的\r\ntype ReadonlyUser = Readonly<User>\r\n/**\r\n * type ReadonlyUser = {\r\n *   readonly name: string;\r\n *   readonly age: number;\r\n *   readonly sex: number;\r\n * }\r\n */\r\n```\r\n\r\n#### Pick\\<T, K\\>\r\n\r\n```ts\r\ntype Pick<T, K extends keyof T> = {\r\n    [P in K]: T[P];\r\n};\r\n```\r\n\r\n`Pick` 用于根据传入类型生成一个包含指定属性的新类型，也就是说，我们可以从旧类型中挑出部分属性来构成一个新的类型。\r\n\r\n```ts\r\ninterface User {\r\n  name: string;\r\n  age: number;\r\n  sex: number;\r\n};\r\n\r\n// PickUser 类型没有 sex 属性\r\ntype PickUser = Pick<User, 'name' | 'age'>\r\n/**\r\n * type PickUser = {\r\n *   name: string;\r\n *   age: number;\r\n * }\r\n */\r\n```\r\n\r\n#### Record\\<T, K\\>\r\n\r\n```ts\r\ntype Record<K extends keyof any, T> = {\r\n    [P in K]: T;\r\n};\r\n```\r\n\r\n`Record` 利用一个字符串字面量类型生成一个对象类型，字符串字面量类型的各项将作为对象类型的属性键名。不同于以上几个类型，`Record` 不是同态的，即它不需要输入类型来拷贝属性，而是直接创建新的属性。\r\n\r\n```ts\r\ntype RecordUser = Record<'name' | 'telephone' | 'description', string>\r\n/**\r\n * type RecordUser = {\r\n *   name: string;\r\n *   telephone: string;\r\n *   description: string;\r\n * }\r\n */\r\n```\r\n\r\n### 条件类型\r\n\r\n另一种常用的泛型结构是条件类型。条件类型能在生成一个新类型时对类型的组合转换提供条件限制。同样的，TS 标准库也内置了一些常用的条件类型，包括 `Exclude`，`Extract`，`ReturnType` 和 `Parameters`等。\r\n\r\n#### Extract\\<T, U\\>\r\n\r\n```ts\r\ntype Extract<T, U> = T extends U ? T : never\r\n```\r\n\r\n`Extract<T, U>` 用于从联合类型 T 中排除联合类型 U 中不存在的成员，生成一个新的联合类型，也就是说生成的联合类型只包含那些 T 有且 U 也有的成员，作用类似于求交集。\r\n\r\n```ts\r\ntype UserKey = Extract<'name' | 'age' | 'sex', 'name' | 'sex' | 'descrption'>\r\n// type UserKey = \"name\" | \"sex\"\r\n```\r\n\r\n\r\n#### Exclude\\<T, U\\>\r\n\r\n```ts\r\ntype Exclude<T, U> = T extends U ? never : T\r\n```\r\n\r\n`Exclude<T, U>` 用于从联合类型 T 中排除联合类型 U 中存在的成员，生成一个新的联合类型，也就是说生成的联合类型只包含那些 T 有而 U 没有的成员，作用类似于求非集。\r\n\r\n```ts\r\ntype UserKey = Exclude<'name' | 'age' | 'sex', 'sex'>\r\n// type UserKey = \"name\" | \"age\"\r\n```\r\n\r\n利用 `Exclude`，我们还可以实现一个和 `Pick` 作用相反的 `NeverPick`：\r\n\r\n```ts\r\ntype NeverPick<T, U> = {\r\n  [P in Exclude<keyof T, U>]: T[P];\r\n};\r\n\r\ninterface User {\r\n  name: string;\r\n  age: number;\r\n  sex: number;\r\n};\r\n\r\ntype NeverPickUser = NeverPick<User, 'sex'>\r\n/**\r\n * type NeverPickUser = {\r\n *   name: string;\r\n *   age: number;\r\n * }\r\n */\r\n```\r\n\r\n#### Parameters\\<T\\>\r\n\r\n```ts\r\ntype Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never\r\n```\r\n\r\n`Parameters` 当传入的指定类型为函数类型时，返回函数入参的类型。这里使用了 `infer` 操作符来表示待推断类型。\r\n\r\n```ts\r\ninterface User {\r\n  name: string;\r\n  age: number;\r\n  sex: number;\r\n};\r\n\r\n// 定义一个函数类型 GetUserFunc，入参 name 的类型是 string\r\ninterface GetUserFunc {\r\n  (name: string): User \r\n}\r\n\r\n// 提取到了 GetUserFunc 的参数类型\r\ntype funcParamsTypes = Parameters<GetUserFunc>\r\n// type funcParamsTypes = [string]\r\n```\r\n\r\n#### ReturnType\\<T\\>\r\n\r\n```ts\r\ntype ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any\r\n```\r\n\r\n`ReturnType` 当传入的指定类型为函数类型时，返回函数返回值的类型。\r\n\r\n```ts\r\ninterface User {\r\n  name: string;\r\n  age: number;\r\n  sex: number;\r\n};\r\n\r\n// 定义一个函数类型 GetUserFunc，返回值的类型是 User\r\ninterface GetUserFunc {\r\n  (name: string): User \r\n}\r\n\r\n// 提取到了 GetUserFunc 的参数类型\r\ntype funcReturnTypes = ReturnType<GetUserFunc>\r\n// type funcReturnTypes = User\r\n```\r\n\r\n## 参考文章\r\n> [Typescript官方文档-高级类型](https://www.tslang.cn/docs/handbook/advanced-types.html)\r\n> [解读TypeScript中的泛型以及条件类型中的推断](https://juejin.im/post/5d0cf23af265da1b855c5ef6#heading-11)"}}
{"actor":{"display_login":"deqwin"}}