[译]<<Effective TypeScript>> 高效TypeScript62个技巧 技巧14

288 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情

本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.

技巧14: 用类型操作,泛型避免重复代码

用命令行打印若干个长方体的: 长宽高, 表面积, 体积:

console.log('Cylinder 1 x 1 ',
  'Surface area:', 6.283185 * 1 * 1 + 6.283185 * 1 * 1,
  'Volume:', 3.14159 * 1 * 1 * 1);
console.log('Cylinder 1 x 2 ',
  'Surface area:', 6.283185 * 1 * 1 + 6.283185 * 2 * 1,
  'Volume:', 3.14159 * 1 * 2 * 1);
console.log('Cylinder 2 x 1 ',
  'Surface area:', 6.283185 * 2 * 1 + 6.283185 * 2 * 1,
  'Volume:', 3.14159 * 2 * 2 * 1);

这些代码看起来很不舒服, 因为重复特别多.不仅重复值还重复了很多常量. 很容易出现拼写错误. 更好的写法,将代码拆分成: 函数, 常量, 循环

const surfaceArea = (r, h) => 2 * Math.PI * r * (r + h);
const volume = (r, h) => Math.PI * r * r * h;
for (const [r, h] of [[1, 1], [1, 2], [2, 1]]) {
  console.log(
    `Cylinder ${r} x ${h}`,
    `Surface area: ${surfaceArea(r, h)}`,
    `Volume: ${volume(r, h)}`);
}

这就是 DRY 原则: 不要重复自己. 同时我们应该避免在写类型的时候重复:

interface Person {
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate {
  firstName: string;
  lastName: string;
  birth: Date;
}

类型代码的重复和普通代码的重复会造成同样的问题. 但是重复的类型代码会更多见, 因为有许多人不熟悉如何对类型代码进行分解成: 函数, 循环.

  1. 抽取常量

    最简单的方法减少类型代码重复, 抽取出 type 后重命名:

    function distance(a: {x: number, y: number}, b: {x: number, y: number}) {
      return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
    }
    

    抽取出一个 type, 重命名:

    interface Point2D {
      x: number;
    y: number;
    }
    function distance(a: Point2D, b: Point2D) { /* ... */ }
    

    这相当于在类型系统中, 分解出一个常量, 而不是重复去写.但是复杂了类型的重复不容易被发现, 因为它们有时候会被复杂语法所隐藏.

    如果多个函数类型一样, 也可以抽取type. 抽取前

    function get(url: string, opts: Options): Promise<Response> { /* ... */ }
    function post(url: string, opts: Options): Promise<Response> { /* ... */ }
    

    抽取后:

    type HTTPFunction = (url: string, opts: Options) => Promise<Response>;
    const get: HTTPFunction = (url, opts) => { /* ... */ };
    const post: HTTPFunction = (url, opts) => { /* ... */ };
    
  2. 扩展.

    对于Person/PersonWithBirthDate 那个例子, 我们可以用extends 来优化:

    interface Person {
      firstName: string;
      lastName: string;
    }
    
    interface PersonWithBirthDate extends Person {
      birth: Date;
    }
    

    当两个 interface 有部分属性相同, 我们可以抽取出一个含有相同属性的interface. 继续分析: 3.141593 and 6.283185应该用PI and 2*PI来代替.

    也可以用交集操作符(&)来扩展 type:

    type PersonWithBirthDate = Person & { birth: Date };
    
  3. 循环:

    当有这样两个 interface:

    interface State {
      userId: string;
      pageTitle: string;
      recentFiles: string[];
      pageContents: string;
    }
    interface TopNavState {
      userId: string;
      pageTitle: string;
      recentFiles: string[];
    }
    

    你希望将 TopNavState的字段 定义为 State 字段的子集, 而不是通过 extends TopNavState 来生成 State.

    那么你可以通过索引来做:

    type TopNavState = {
      userId: State['userId'];
      pageTitle: State['pageTitle'];
      recentFiles: State['recentFiles'];
    };
    

    这样依旧有部分重复代码, 我们可以用循环来改进:

    type TopNavState = {
      [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
    };
    
  4. 泛型.
    上一个部分我们可以用循环来生成type, 实际使用中我们会将泛型和循环结合起来.
    这种方法在标准库中非常常见:

    type Pick<T, K> = { [k in K]: T[k] };
    

    我们可以这样使用:

    type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
    

    泛型就好像我们普通代码的函数.

  5. 标记的联合: 另外一种常见的重复在于标记的联合, 例如:

    interface SaveAction {
      type: 'save';
      // ...
    }
    interface LoadAction {
      type: 'load';
      // ...
    }
    type Action = SaveAction | LoadAction;
    type ActionType = 'save' | 'load';  // Repeated types!
    

    我们可以这样解决重复:

    type ActionType = Action['type'];  // Type is "save" | "load"
    

    这样使用和使用pick泛型得到的标记联合是不一样的:

    type ActionRec = Pick<Action, 'type'>; 
    // {type: "save" | "load"}
    
  6. update type 另外一种常见的重复: 我们定义了一个class, 随后还需要对这个class进行升级. 升级的参数有可能能是可选参数:

    interface Options {
      width: number;
      height: number;
      color: string;
      label: string;
    }
    interface OptionsUpdate {
      width?: number;
      height?: number;
      color?: string;
      label?: string;
    }
    class UIWidget {
      constructor(init: Options) { /* ... */ }
      update(options: OptionsUpdate) { /* ... */ }
    }
    

    我们可以用循环来减少OptionsUpdate的重复:

    type OptionsUpdate = {[k in keyof Options]?: Options[k]};
    

    keyof 用来获取标记的联合:

    type OptionsKeys = keyof Options;
    // Type is "width" | "height" | "color" | "label"
    

    这种方法非常常见, 以至于抽象成一个泛型放入了标准库:

    class UIWidget {
      constructor(init: Options) { /* ... */ }
      update(options: Partial<Options>) { /* ... */ }
    }
    
  7. 由初始值快速生成type.

    我们有时会需要初始值来生成相应的type:

    const INIT_OPTIONS = {
      width: 640,
      height: 480,
      color: '#00FF00',
      label: 'VGA',
    };
    interface Options {
      width: number;
      height: number;
      color: string;
      label: string;
    }
    

    我们可以这样生成:

    type Options = typeof INIT_OPTIONS;
    

    注意: 小心从 values 中生成 types. 更安全的办法就是先定义type, 在定义values的时候指定type.

    相似的, 你可能想根据函数的返回值得到一个type,例如:

    function getUserInfo(userId: string) {
      // ...
      return {
        userId,
        name,
        age,
        height,
        weight,
        favoriteColor,
      };
    }
    // Return type inferred as { userId: string; name: string; age: number, ... }
    

    标准库中有一个泛型 ReturnType 可以直接得到返回值的type:

    type UserInfo = ReturnType<typeof getUserInfo>;
    
  8. 对泛型的参数进行约束.

    我们可以用 extends 对泛型的参数进行约束:

    interface Name {
      first: string;
      last: string;
    }
    type DancingDuo<T extends Name> = [T, T];
    
    const couple1: DancingDuo<Name> = [
      {first: 'Fred', last: 'Astaire'},
     {first: 'Ginger', last: 'Rogers'}
    ];  // OK
    const couple2: DancingDuo<{first: string}> = [
                           // ~~~~~~~~~~~~~~~
                           // Property 'last' is missing in type
                           // '{ first: string; }' but required in type 'Name'
      {first: 'Sonny'},
      {first: 'Cher'}
    ];
    

    我们还可以用泛型约束对之前的pick函数进一步改进:

    type Pick<T, K extends keyof T> = {
      [k in K]: T[k]
    };  // O
    

    改进后我们能对参数实现更好的约束.:

    type FirstLast = Pick<Name, 'first' | 'last'>;  // OK
    type FirstMiddle = Pick<Name, 'first' | 'middle'>;
                               // ~~~~~~~~~~~~~~~~~~
                               // Type '"middle"' is not assignable
                               // to type '"first" | "last"'