《Effective TypeScript》读书笔记(一)

第一章:了解 TypeScript

第1条 理解 TypeScript 和 JavaScript 之间的关系

第2条 了解你在使用的 TypeScript 配置

打开 tsconfig.json 中的 noImplicitAnystrictNullChecks 配置,尽可能避免运行时出现 undefined is not an object 等错误。

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

在初始化变量时,如果赋值为 nullundefined,明确标出对应类型。

const x: number = null;
const y: number = undefined;
const x: number | null = null;
const y: number | undefined = undefined;

第3条 理解代码生成和类型两者互相独立

无法在运行时检查 TypeScript 类型

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'.
    // Property 'height' does not exist on type 'Square'.
  } else {
    return shape.width * shape.width;
  }
}

TypeScript 编译成 JavaScript 后,类型信息会被擦除。可以通过检查结构的写法代替:

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 代替 interface,这样就可以使用 instanceof 进行判断:

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

运行时类型和声明的类型可能不一样

function setLightSwitch(value: boolean) {
  switch (value) {
    case true:
      turnLightOn();
      break;
    case false:
      turnLightOff();
      break;
    default:
      console.log(`I'm afraid I can't do that.`);
  }
}

function turnLightOn() {
  // ...
}
function turnLightOff() {
  // ...
}

interface LightApiResponse {
  lightSwitchValue: boolean;
}

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

无法保证数据请求的结果和 TS 声明的 LightApiResponse 结构完全一致,如果响应中的 lightSwitchValue 为字符串 "true"null,或者直接缺少该字段,都会产生预期之外的错误。因此在输入外部不可控的数据源时,依然需要防御性编程。

无法基于 TypeScript 类型实现函数重载

TypeScript 虽然提供了类型,但无法实现 C++ 等语言中的函数重载(Function Overload):

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

TypeScript 的重载只针对类型标注,允许有多个函数声明,但只能有一个实现:

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any) {
  return a + b;
}
const three = add(1, 2); // Type is number
const twelve = add('1', '2'); // Type is string

第4条 适应结构化类型

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

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

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

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

TypeScript 的类型被称为结构化类型(Structural Typing),和 C++、Java 不同,即使 NamedVector 不是继承于 Vector2D,依然可以正常调用 calculateLength

当然这种设计也有它存在的问题,对于如下代码:

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

function normalize(v: Vector3D) {
  const length = calculateLength(v);
  return {
    x: v.x / length,
    y: v.y / length,
    z: v.z / length,
  };
}

normalize({ x: 3, y: 4, z: 5 }); // result is { x: 0.6, y: 0.8, z: 1 }

调用 normalize 时不会发现向量的 z 属性没有被 calculateLength 处理。因为只要传入的参数符合 Vector3D 结构,都视为合法。像下面的代码,在 TypeScript 会报类型错误:

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 expression of type 'string'
    // can't be used to index type 'Vector3D'. No index signature with a 
    // parameter of type 'string' was found on type 'Vector3D'.
    length += Math.abs(coord);
  }
  return length;
}

const vec3D = { x: 3, y: 4, z: 1, address: '123 Broadway' };
calculateLengthL1(vec3D); // OK, returns NaN

可以重写为:

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

第5条 限制 any 类型的使用