[译]<<Effective TypeScript>> 高效TypeScript62个技巧 技巧1-5

755 阅读8分钟

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

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

了解 TypeScript

技巧1: TypeScript 和 JavaScript 的关系

ts 是 js 的超集, 这个超集包括几个特点:

  • js 代码天然就是ts代码, 下面用 ts-node-dev 运行, 能顺利运行

    function greet(who) {
      console.log('Hello', who);
    }
    
    greet('lala')
    
  • ts代码却不一定是合法的 js代码, ts有自己独特的语法

ts 增加了类型系统 , 用来在js运行前发现错误:

let city = 'new york city';
console.log(city.toUppercase());
             // ~~~~~~~~~~~ Property 'toUppercase' does not exist on type
             //             'string'. Did you mean 'toUpperCase'?

但是ts不能完全发现错误, 例如ts无法发现数组越界的错误

const names = ['Alice', 'Bob'];
console.log(names[2].toUpperCase());

部分js语法在ts中会被禁止使用:

const a = null + 7;  // Evaluates to 7 in JS
       // ~~~~ Operator '+' cannot be applied to types ...
const b = [] + 12;  // Evaluates to '12' in JS
       // ~~~~~~~ Operator '+' cannot be applied to types ...
alert('Hello', 'TypeScript');  // alerts "Hello"
            // ~~~~~~~~~~~~ Expected 0-1 arguments, but got 2

技巧2: 了解 ts 的配置

一个项目的ts配置,可以在项目根目录下的tsconfig.json文件中查看. 其中两个重要的配置需要了解: noImplicitAnystrictNullChecks

  • noImplicitAny

    当编辑tsconfig.json文件:

    {
      "compilerOptions": {
        "noImplicitAny": false
      }
    }
    

    下面代码是可以通过ts的类型检查的:

    function add(a, b) {
      return a + b;
    }
    add(10, null);
    

    当编辑tsconfig.json文件:

    {
      "compilerOptions": {
        "noImplicitAny": true
      }
    }
    

    会出现报错:

    function add(a, b) {
              // ~    Parameter 'a' implicitly has an 'any' type
              //    ~ Parameter 'b' implicitly has an 'any' type
      return a + b;
    }
    
    

    这有利于我们更好的捕捉错误.所以我们应该尽量将noImplicitAny设置为true

  • strictNullChecks
    当编辑tsconfig.json文件:

    {
      "compilerOptions": {
        "strictNullChecks": false
      }
    }
    

    下面代码是可以通过ts的类型检查的:

    const x: number = null;
    

    当编辑tsconfig.json文件:

    {
      "compilerOptions": {
        "strictNullChecks": true
      }
    }
    

    会出现报错:

    const x: number = null;
    //    ~ Type 'null' is not assignable to type 'number'
    
    

    这有利于我们更好的避免nullundefine引起的错误.所以我们应该尽量将strictNullChecks设置为true

技巧3: 理解ts的代码生成与类型检查之间的相互独立

tsc(ts的编译器)只做两件事:

  • 将ts代码转成低版本的js代码,以便运行在浏览器上
  • 检查你代码中的类型错误 记得这一点: 上述两件事是完全独立的!

独立会造成什么影响?

  1. 影响一: 当你的代码类型错误, 依旧会生成js文件

    $ cat test.ts
    let x = 'hello';
    x = 1234;
    
    $ tsc test.ts
    test.ts:2:1 - error TS2322: Type '1234' is not assignable to type 'string'
    
    2 x = 1234;
      ~
    
    $ cat test.js
    var x = 'hello';
    x = 1234;
    

    这样有利有弊

    好处在于 当我们项目急着上线, 那我们可以先生成可运行的js代码, 之后有时间再来修改类型错误

    不好在于 你不知道哪天这个会变成严重的错误

    如果我们目标定为0错误才生成js代码.那么我们可以在tsconfig.json文件中设置noEmitOnError为 true

  2. 影响二: 我们不能在代码运行检查ts的类型

    例如下面代码:

    interface Square {
      width: number;
    }
    interface Rectangle extends Square {
      height: number;
    }
    type Shape = Square | Rectangle;
    
    function calculateArea(shape: Shape) {
      if (shape instanceof Rectangle) {
                        // ~~~~~~~~~ 'Rectangle' only refers to a type,
                        //           but is being used as a value here
        return shape.width * shape.height;
                        //         ~~~~~~ Property 'height' does not exist
                        //                on type 'Shape'
      } else {
        return shape.width * shape.width;
      }
    }
    

    instanceof 用于检查变量类型, 但是它只会在代码运行时候检查. 但是在代码运行前, tsc在生成js文件的时候, 会将所有ts 的types, interfaces 擦除.

    对于这类问题可以这么解决:

    第一种方法:

    function calculateArea(shape: Shape) {
      if ('height' in shape) {
        shape;  // Type is Rectangle
        return shape.width * shape.height;
      } else {
        shape;  // Type is Square
        return shape.width * shape.width;
      }
    }
    

    第二种方法:

    interface Square {
      kind: 'square';
      width: number;
    }
    interface Rectangle {
      kind: 'rectangle';
      height: number;
      width: number;
    }
    type Shape = Square | Rectangle;
    
    function calculateArea(shape: Shape) {
      if (shape.kind === 'rectangle') {
        shape;  // Type is Rectangle
        return shape.width * shape.height;
      } else {
        shape;  // Type is Square
        return shape.width * shape.width;
      }
    }
    

    第三种方法:

    class Square {
      constructor(public width: number) {}
    }
    class Rectangle extends Square {
      constructor(public width: number, public height: number) {
        super(width);
      }
    }
    type Shape = Square | Rectangle;
    
    function calculateArea(shape: Shape) {
      if (shape instanceof Rectangle) {
        shape;  // Type is Rectangle
        return shape.width * shape.height;
      } else {
        shape;  // Type is Square
        return shape.width * shape.width;  // OK
      }
    }
    
  3. 影响三:类型操作无法影响运行时变量的值

    例如下面代码:

    function asNumber(val: number | string): number {
      return val as number;
    }
    

    tsc会生成如下代码:

    function asNumber(val) {
      return val;
    }
    

    我们可以这样解决:

    function asNumber(val: number | string): number {
      return typeof(val) === 'string' ? Number(val) : 
        val;
    }
    
  4. 影响四:代码运行时变量的类型, 和我们预先声明的类型,可能不一样 这类问题常见与从接口中获取数据:

    interface LightApiResponse {
      lightSwitchValue: boolean;
    }
    async function setLight() {
      const response = await fetch('/light');
      const result: LightApiResponse = await response.json();
      setLightSwitch(result.lightSwitchValue);
    }
    

    我们从接口中获取的response不一定是我们预先定义的LightApiResponse类型

  5. 影响五: 我们不能通过ts的类型来申明函数重载

    下列代码会报错:

    function add(a: number, b: number) { return a + b; }
          // ~~~ Duplicate function implementation
    function add(a: string, b: string) { return a + b; }
          // ~~~ Duplicate function implementation
    

    我们可以通过下面的方法申明函数重载:

    function add(a: number, b: number): number;
    
    function add(a: string, b: string): string;
    

function add(a, b) {

  return a + b;
}

const three = add(1, 2);  // Type is number
const twelve = add('1', '2');  // Type is string
```

6. 影响六: ts 的类型系统不会代码影响代码运行性能 所有ts的类型定义, 操作都会在运行前被擦除, 所以ts的类型系统不会对代码运行带来任何负担

技巧四: 熟悉ts的类型兼容

掌握ts的类型兼容就能写出更健壮的代码.

假设你有一个二维的type:

interface Vector2D {
  x: number;
  y: number;
}

你写了一个函数计算该向量到原点的距离:

function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y);
}

同时你还有一个可以命名的二维的type

interface NamedVector {
  name: string;
  x: number;
  y: number;
}

尝试运行一下, 下列代码, 发现能正常运行.

const v: NamedVector = { x: 3, y: 4, name: 'Zee' };
calculateLength(v);  // OK, result is 5

这里可以得出结论:

NamedVectorVector2D所有的属性, 那么函数calculateLength如果能够接受Vector2D类型的变量, 那也一定能接受NamedVector类型的变量.

但是这个类型兼容会导致一个问题:

例如当你有一个三维的type:

interface Vector3D {
  x: number;
  y: number;
  z: number;
}

如果你想计算三维空间的点到原点的距离,正确的计算公式应该是: Math.sqrt(v.x * v.x + v.y * v.y + v.z*v.z)

直接调用上面的calculateLength函数显然是错误的, 但是ts并不会报错!, 因为其满足ts类型兼容型原则

所以请记住一个原则: 尽量不要对遍历一个对象

请看如下的代码

function calculateLengthL1(v: Vector3D) {
  let length = 0;
  for (const axis of Object.keys(v)) {
    const coord = v[axis];
               // ~~~~~~~ Element implicitly has an 'any' type because ...
               //         'string' can't be used to index type 'Vector3D'
    length += Math.abs(coord);
  }
  return length;
}

为什么会报错? 因为你虽然定义了变量v是Vector3D类型, 但是由于类型兼容原则. 你无法保证, 用户会传什么类型的数据.

例如:下面这个类型也能传入函数calculateLengthL1

interface VectorND {
  x: number;
  y: number;
  z: number;
  p: number;
  s: string;
}

那我们改如何解决这个问题, 不用循环来遍历对象:

function calculateLengthL1(v: Vector3D) {
  return Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z);
}

类型兼容当遇到class也有可能出问题:

class C {
  foo: string;
  constructor(foo: string) {
    this.foo = foo;
  }
}

const c = new C('instance of C');
const d: C = { foo: 'object literal' };  // OK!

由于类型兼容, 导致d也能分配给C

但是类型兼容对写测试用例其实是有好处的! 例如你写了一个函数查询数据集,并且处理结果:

interface Author {
  first: string;
  last: string;
}
function getAuthors(database: PostgresDB): Author[] {
  const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
  return authorRows.map(row => ({first: row[0], last: row[1]}));
}

想要测试这个函数, 你当然可以写PostgresDB类型数据的mock, 但是有一个更好的办法:

interface DB {
  runQuery: (sql: string) => any[];
}
function getAuthors(database: DB): Author[] {
const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
  return authorRows.map(row => ({first: row[0], last: row[1]}));
}

在使用这个函数的时候, 可以传入PostgresDB类型的数据. 但是在写测试用例的时候将变得更容易:

test('getAuthors', () => {
  const authors = getAuthors({
    runQuery(sql: string) {
      return [['Toni', 'Morrison'], ['Maya', 'Angelou']];
    }
  });
  expect(authors).toEqual([
    {first: 'Toni', last: 'Morrison'},
    {first: 'Maya', last: 'Angelou'}
  ]);
});

技巧五: 避免使用any

使用any能够让你逃过类型检查, 例如:

let age: number;
   age = '12';
// ~~~ Type '"12"' is not assignable to type 'number'
   age = '12' as any;  // OK

但是请你务必要了解使用any带来的危害:

  1. 使用any将失去ts的类型安全检查.

    age += 1;  // OK; at runtime, age is now "121"
    
  2. 使用any,可能会导致js偷偷转换你数据的类型

    function calculateAge(birthDate: Date): number {
      // ...
    }
    
    let birthDate: any = '1990-01-19';
    calculateAge(birthDate);  // OK
    

    你传入birthDate是一个字符串, 但是有可能在函数中被转换为number

  3. 将失去ts带来的一些便利

    使用ts能帮助代码编辑器, 实现代码自动补全功能, 例如:

    image.png

  4. any 影藏了你的类型设计 设计一个好的类型, 对于一个干净, 健壮, 简洁的代码非常重要, 用了any, 你的同事或者合作伙伴看不到你的类型设计