十二、TypeScript 泛型

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。
泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

12.1 泛型接口

  1. interface GenericIdentityFn<T> {
  2. (arg: T): T;
  3. }

12.2 泛型类

  1. class GenericNumber<T> {
  2. zeroValue: T;
  3. add: (x: T, y: T) => T;
  4. }
  5. let myGenericNumber = new GenericNumber<number>();
  6. myGenericNumber.zeroValue = 0;
  7. myGenericNumber.add = function (x, y) {
  8. return x + y;
  9. };

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 操作符可以用来获取一个变量声明或对象的类型。 ```typescript interface Person { name: string; age: number; } const sem: Person = { name: ‘semlinker’, age: 30 }; type Sem= typeof sem; // -> Person function toArray(x: number): Array { return [x]; } type Func = typeof toArray; // -> (x: number) => number[]
  1. **2.keyof**<br />`keyof` 操作符可以用来一个对象中的所有 key 值:
  2. ```typescript
  3. interface Person {
  4. name: string;
  5. age: number;
  6. }
  7. type K1 = keyof Person; // "name" | "age"
  8. type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
  9. type K3 = keyof { [x: string]: Person }; // string | number

3.in
in 用来遍历枚举类型:

  1. type Keys = "a" | "b" | "c"
  2. type Obj = {
  3. [p in Keys]: any
  4. } // -> { a: any, b: any, c: any }

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

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

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。
5.extends
有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。

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

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

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

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

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

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

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

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

  1. interface Todo {
  2. title: string;
  3. description: string;
  4. }
  5. function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  6. return { ...todo, ...fieldsToUpdate };
  7. }
  8. const todo1 = {
  9. title: "organize desk",
  10. description: "clear clutter",
  11. };
  12. const todo2 = updateTodo(todo1, {
  13. description: "throw out trash",
  14. });

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

  1. {
  2. title?: string | undefined;
  3. description?: string | undefined;
  4. }

十三、TypeScript 装饰器

13.1 装饰器是什么

  • 它是一个表达式
  • 该表达式被执行后,返回一个函数
  • 函数的入参分别为 target、name 和 descriptor
  • 执行该函数后,可能返回 descriptor 对象,用于配置 target 对象

    13.2 装饰器的分类

  • 类装饰器(Class decorators)

  • 属性装饰器(Property decorators)
  • 方法装饰器(Method decorators)
  • 参数装饰器(Parameter decorators)

    13.3 类装饰器

    类装饰器声明: ```typescript declare type ClassDecorator = ( target: TFunction ) => TFunction | void;
  1. 类装饰器顾名思义,就是用来装饰类的。它接收一个参数:
  2. - target: TFunction - 被装饰的类
  3. 看完第一眼后,是不是感觉都不好了。没事,我们马上来个例子:
  4. ```typescript
  5. function Greeter(target: Function): void {
  6. target.prototype.greet = function (): void {
  7. console.log("Hello Semlinker!");
  8. };
  9. }
  10. @Greeter
  11. class Greeting {
  12. constructor() {
  13. // 内部实现
  14. }
  15. }
  16. let myGreeting = new Greeting();
  17. myGreeting.greet(); // console output: 'Hello Semlinker!';

上面的例子中,我们定义了 Greeter 类装饰器,同时我们使用了 @Greeter 语法糖,来使用装饰器。

友情提示:读者可以直接复制上面的代码,在 TypeScript Playground 中运行查看结果。

有的读者可能想问,例子中总是输出 Hello Semlinker! ,能自定义输出的问候语么 ?这个问题很好,答案是可以的。
具体实现如下:

  1. function Greeter(greeting: string) {
  2. return function (target: Function) {
  3. target.prototype.greet = function (): void {
  4. console.log(greeting);
  5. };
  6. };
  7. }
  8. @Greeter("Hello TS!")
  9. class Greeting {
  10. constructor() {
  11. // 内部实现
  12. }
  13. }
  14. let myGreeting = new Greeting();
  15. myGreeting.greet(); // console output: 'Hello TS!';

13.4 属性装饰器

属性装饰器声明:

  1. declare type PropertyDecorator = (target:Object,
  2. propertyKey: string | symbol ) => void;
  3. 复制代码

属性装饰器顾名思义,用来装饰类的属性。它接收两个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 被装饰类的属性名

趁热打铁,马上来个例子热热身:

  1. function logProperty(target: any, key: string) {
  2. delete target[key];
  3. const backingField = "_" + key;
  4. Object.defineProperty(target, backingField, {
  5. writable: true,
  6. enumerable: true,
  7. configurable: true
  8. });
  9. // property getter
  10. const getter = function (this: any) {
  11. const currVal = this[backingField];
  12. console.log(`Get: ${key} => ${currVal}`);
  13. return currVal;
  14. };
  15. // property setter
  16. const setter = function (this: any, newVal: any) {
  17. console.log(`Set: ${key} => ${newVal}`);
  18. this[backingField] = newVal;
  19. };
  20. // Create new property with getter and setter
  21. Object.defineProperty(target, key, {
  22. get: getter,
  23. set: setter,
  24. enumerable: true,
  25. configurable: true
  26. });
  27. }
  28. class Person {
  29. @logProperty
  30. public name: string;
  31. constructor(name : string) {
  32. this.name = name;
  33. }
  34. }
  35. const p1 = new Person("semlinker");
  36. p1.name = "kakuqo";
  37. 复制代码

以上代码我们定义了一个 logProperty 函数,来跟踪用户对属性的操作,当代码成功运行后,在控制台会输出以下结果:

  1. Set: name => semlinker
  2. Set: name => kakuqo

13.5 方法装饰器

方法装饰器声明:

  1. declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,
  2. descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;

方法装饰器顾名思义,用来装饰类的方法。它接收三个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 方法名
  • descriptor: TypePropertyDescript - 属性描述符

废话不多说,直接上例子:

  1. function LogOutput(tarage: Function, key: string, descriptor: any) {
  2. let originalMethod = descriptor.value;
  3. let newMethod = function(...args: any[]): any {
  4. let result: any = originalMethod.apply(this, args);
  5. if(!this.loggedOutput) {
  6. this.loggedOutput = new Array<any>();
  7. }
  8. this.loggedOutput.push({
  9. method: key,
  10. parameters: args,
  11. output: result,
  12. timestamp: new Date()
  13. });
  14. return result;
  15. };
  16. descriptor.value = newMethod;
  17. }
  18. class Calculator {
  19. @LogOutput
  20. double (num: number): number {
  21. return num * 2;
  22. }
  23. }
  24. let calc = new Calculator();
  25. calc.double(11);
  26. // console ouput: [{method: "double", output: 22, ...}]
  27. console.log(calc.loggedOutput);

下面我们来介绍一下参数装饰器。

13.6 参数装饰器

参数装饰器声明:

  1. declare type ParameterDecorator = (target: Object, propertyKey: string | symbol,
  2. parameterIndex: number ) => void

参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 方法名
  • parameterIndex: number - 方法中参数的索引值 ``typescript function Log(target: Function, key: string, parameterIndex: number) { let functionLogged = key || target.prototype.constructor.name; console.log(The parameter in position ${parameterIndex} at ${functionLogged} has been decorated`); } class Greeter { greeting: string; constructor(@Log phrase: string) { this.greeting = phrase; } } // console output: The parameter in position 0 // at Greeter has been decorated
  1. 介绍完 TypeScript 入门相关的基础知识,猜测很多刚入门的小伙伴已有 **“从入门到放弃”** 的想法,最后我们来简单介绍一下编译上下文。
  2. <a name="PROlj"></a>
  3. ### 十四、编译上下文
  4. <a name="vLYTS"></a>
  5. #### 14.1 tsconfig.json 的作用
  6. - 用于标识 TypeScript 项目的根路径;
  7. - 用于配置 TypeScript 编译器;
  8. - 用于指定编译的文件。
  9. <a name="HZv39"></a>
  10. #### 14.2 tsconfig.json 重要字段
  11. - files - 设置要编译的文件的名称;
  12. - include - 设置需要进行编译的文件,支持路径模式匹配;
  13. - exclude - 设置无需进行编译的文件,支持路径模式匹配;
  14. - compilerOptions - 设置与编译流程相关的选项。
  15. <a name="psMYa"></a>
  16. #### 14.3 compilerOptions 选项
  17. compilerOptions 支持很多选项,常见的有 `baseUrl` `target``baseUrl` `moduleResolution` `lib` 等。<br />compilerOptions 每个选项的详细说明如下:
  18. ```typescript
  19. {
  20. "compilerOptions": {
  21. /* 基本选项 */
  22. "target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
  23. "module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
  24. "lib": [], // 指定要包含在编译中的库文件
  25. "allowJs": true, // 允许编译 javascript 文件
  26. "checkJs": true, // 报告 javascript 文件中的错误
  27. "jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
  28. "declaration": true, // 生成相应的 '.d.ts' 文件
  29. "sourceMap": true, // 生成相应的 '.map' 文件
  30. "outFile": "./", // 将输出文件合并为一个文件
  31. "outDir": "./", // 指定输出目录
  32. "rootDir": "./", // 用来控制输出目录结构 --outDir.
  33. "removeComments": true, // 删除编译后的所有的注释
  34. "noEmit": true, // 不生成输出文件
  35. "importHelpers": true, // 从 tslib 导入辅助工具函数
  36. "isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).
  37. /* 严格的类型检查选项 */
  38. "strict": true, // 启用所有严格类型检查选项
  39. "noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
  40. "strictNullChecks": true, // 启用严格的 null 检查
  41. "noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
  42. "alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
  43. /* 额外的检查 */
  44. "noUnusedLocals": true, // 有未使用的变量时,抛出错误
  45. "noUnusedParameters": true, // 有未使用的参数时,抛出错误
  46. "noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
  47. "noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
  48. /* 模块解析选项 */
  49. "moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
  50. "baseUrl": "./", // 用于解析非相对模块名称的基目录
  51. "paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
  52. "rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
  53. "typeRoots": [], // 包含类型声明的文件列表
  54. "types": [], // 需要包含的类型声明文件名列表
  55. "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
  56. /* Source Map Options */
  57. "sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
  58. "mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
  59. "inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
  60. "inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
  61. /* 其他选项 */
  62. "experimentalDecorators": true, // 启用装饰器
  63. "emitDecoratorMetadata": true // 为装饰器提供元数据的支持
  64. }
  65. }

看到这里的读者都是“真爱”,如果你还意犹未尽,那就来看看本人整理的 Github 上 1.5K+ 的开源项目:awesome-typescript

github.com/semlinker/a…

十五、参考资源