添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
风流的大蒜  ·  OpenXML Not able to ...·  3 小时前    · 
有腹肌的煎饼果子  ·  Convert a ...·  3 小时前    · 
想发财的苹果  ·  Java String Split by ...·  3 小时前    · 
重情义的蟠桃  ·  java ...·  7 小时前    · 
不拘小节的作业本  ·  醫砭 » 資料庫·  1 月前    · 
强健的大白菜  ·  Android RecyclerView ...·  3 月前    · 
儒雅的冲锋衣  ·  Solidworks Flow ...·  11 月前    · 
没读研的冰淇淋  ·  DoCmd.TransferDatabase ...·  1 年前    · 

《Effective TypeScript》读书笔记(三)

第二章:TypeScript 的类型系统

第 11 条 认识到额外属性检查的局限

使用对象字面量赋值时,TypeScript 会进行额外属性检查(Excess Property Checking):

interface Room {
  numDoors: number;
  ceilingHeightFt: number;
const r: Room = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present',
  // ~~~~~~~~~~~~~~~~~~ Object literal may only specify known properties,
  // and 'elephant' does not exist in type 'Room'

这不太符合 TypeScript 结构化类型的表现,但是如果定义一个中间变量,再进行赋值,则不会报错:

const obj = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present',
const r: Room = obj; // OK

因为 obj 被推导为如下类型,它是 Room 的子类型。

const obj: {
  numDoors: number;
  ceilingHeightFt: number;
  elephant: string;

再看个例子体会一下:

interface Options {
  title: string;
  darkMode?: boolean;
function createWindow(options: Options) {
  if (options.darkMode) {
    // setDarkMode();
  // ...
createWindow({
  title: 'Spider Solitaire',
  darkmode: true
  // ~~~~~~~~~~~~~ Object literal may only specify known properties, but
  // 'darkmode' does not exist in type 'Options'.
  // Did you mean to write 'darkMode'?
});
const o1: Options = document; // OK
const o2: Options = new HTMLAnchorElement; // OK
const o: Options = { darkmode: true, title: 'Ski Free' };
// ~~~~~~~~ 'darkmode' does not exist in type 'Options'...
const intermediate = { darkmode: true, title: 'Ski Free' };
const o: Options = intermediate; // OK
const o = { darkmode: true, title: 'Ski Free' } as Options; // OK

额外属性检查可以避免开发者打错字,在检查的过程中需要遍历属性,有一定的性能开销,所以 TypeScript 只检查字面量不检查中间变量。利用这一点可以绕过 TypeScript 的属性检查,除此以外还可以使用类型断言绕开检查。

第 12 条 尽可能将类型应用到整个函数表达式

如果多个函数的函数签名和返回值类型一致,可以抽出公共的类型声明,简化代码。

function add(a: number, b: number) { return a + b; }
function sub(a: number, b: number) { return a - b; }
function mul(a: number, b: number) { return a * b; }
function div(a: number, b: number) { return a / b; }

可以简化为:

type BinaryFn = (a: number, b: number) => number;
const add: BinaryFn = (a, b) => a + b;
const sub: BinaryFn = (a, b) => a - b;
const mul: BinaryFn = (a, b) => a * b;
const div: BinaryFn = (a, b) => a / b;

再比如自定义一个带状态检查的 fetch 函数,除了检查 response,它的入参和返回值和原生的 fetch 一致:

async function checkedFetch(input: RequestInfo, init?: RequestInit) {
  const response = await fetch(input, init);
  if (!response.ok) {
    // Converted to a rejected Promise in an async function
    throw new Error('Request failed: ' + response.status);
  return response;

其中的 RequestInfoRequestInit 有点冗余,可以简化为:

const checkedFetch: typeof fetch = async (input, init) => {
  const response = await fetch(input, init);
  if (!response.ok) {
    throw new Error('Request failed: ' + response.status);
  return response;

使用函数表达式(Function Expression)代替函数语句(Function Statement),可以更方便地复用函数的类型声明。

第 13 条 知道 type 和 interface 的区别

typeinterface 在大部分场景下可以混用,它们有很多的相同点也有一些不同之处。

interface IStateWithPop extends TState {
  population: number;
type TStateWithPop = IState & { population: number };
  • 都可以表示对象
  • type TState = {
      name: string;
      capital: string;
    interface IState {
      name: string;
      capital: string;
    
  • 都可以表示函数
  • type TFn = (x: number) => string;
    interface IFn {
      (x: number): string;
    
  • 都可以使用泛型
  • type TPair<T> = {
      first: T;
      second: T;
    interface IPair<T> {
      first: T;
      second: T;
    
  • 表示联合类型(Union Types)和交叉类型(Intersection Types),只能用 type
  • type AorB = 'a' | 'b';
    type ColorfulCircle = { color: string } & { radius: number };

    在“第 7 条 将类型看作是值的集合”提过,交叉类型可以换个写法用 extends 表示:

    interface Circle {
      radius: number;
    interface ColorfulCircle extends Circle {
      color: string;
    
  • 表示条件类型(Conditional Types),只能用 type
  • type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
    type ToArray<Type> = Type extends any ? Type[] : never;
    type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
  • 表示元组和数组,只能用 type
  • type Pair = [number, number];
    type StringList = string[];
    type NamedNums = [string, ...number[]];
  • 表示别名(Alias),只能用 type
  • type Second = number;
    type UserInputSanitizedString = string;

    在 TypeScript 文档中 type 被称为 Type Alias,实际上所谓的使用 type 关键字定义类型,其实是给等号右侧的类型、类型表达式起别名。

  • interface 支持声明合并(Declaration Merging),type 不支持
  • interface State {
      name: string;
      capital: string;
    interface State {
      population: number;
    
    type State {
      name: string;
      capital: string;
    type State {
      population: number;
    // ~~~~~~~~ Duplicate identifier 'State'.

    第 14 条 使用类型操作和范型来避免重复

  • 抽出公共的类型
  • 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));
    
    interface Point2D {
      x: number;
      y: number;
    function distance(a: Point2D, b: Point2D) {
      return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
    
  • 复用已有类型
  • interface State {
      userId: string;
      pageTitle: string;
      recentFiles: string[];
      pageContents: string;
    interface TopNavState {
      userId: string;
      pageTitle: string;
      recentFiles: string[];
    
    interface State {
      userId: string;
      pageTitle: string;
      recentFiles: string[];
      pageContents: string;
    type TopNavState = {
      [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k];
    // 可以直接使用内置的 `Pick`
    type TopNavState2 = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
    interface SaveAction {
      type: 'save';
      // ...
    interface LoadAction {
      type: 'load';
      // ...
    type ActionType = 'save' | 'load';
    type ActionType = Action['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;

    对于如下函数:

    function getUserInfo(userId: string) {
      // ...
      return {
        userId,
        name,
        age,
        height,
        weight,
        favoriteColor,
    

    要获得它的返回值类型,可以借助 ReturnType 免去手动定义。

    type UserInfo = ReturnType<typeof getUserInfo>;

    第 15 条 动态数据使用索引签名

    TypeScript 中描述对象类型时可以使用索引签名(Index Signature)简化定义。

    比如以下对象:

    const rocket = {
      name: 'Falcon 9',
      variant: 'v1.0',
      thrust: '4,940 kN',
    

    可以定义为:

    type Rocket = { [property: string]: string };

    索引签名的典型场景是用于处理动态数据,比如解析 CSV 数据:

    function parseCSV(input: string): { [columnName: string]: string }[] {
      const lines = input.trim().split('\n');
      const [header, ...rows] = lines;
      const headerKeys = header.split(',');
      return rows.map(rowStr => {
        const row: { [columnName: string]: string } = {};
        rowStr.split(',').forEach((cell, i) => {
          if (i < headerKeys.length) {
            row[headerKeys[i]] = cell;
        });
        return row;
      });
    

    第 16 条 使用 Array, Tuple, ArrayLike 时偏向于使用数字作为索引签名

    在 TypeScript 的类型定义中,数组被表示为以下形式,其中每一项索引都是数字。

    interface Array<T> {
      length: number;
      [index: number]: T;
    

    访问数组元素时,只能使用数字索引,不能使用数字字符串:

    const xs = [1, 2, 3];
    const x0 = xs[0]; // OK
    const x1 = xs['1'];
    // ~~~ Element implicitly has an 'any' type
    // because index expression is not of type 'number'

    但这不代表使用 for in 循环或者 Object.keys() 获取到的索引类型是 number

    const xs = [1, 2, 3];
    for (const index in xs) {
      index; // Type is string
    const keys = Object.keys(xs); // Type is string[]

    数组归根结底是对象,所以它们的键名是字符串而不是数字,这和 JavaScript 一致。

    第 17 条 使用 readonly 来避免数据变化相关的错误

    如果编写的函数不会修改它的入参,可以在声明入参的时候加上 readonly,避免实现时不经意改动参数。

    比如以下求和函数:

    function arraySum(arr: number[]) {
      let sum = 0, num;
      while ((num = arr.pop()) !== undefined) {
        sum += num;
      return sum;
    

    在实现时没有考虑数据不可变,修改了入参,调用完原数组会被清空。如果加上 readonly 可以及时发现错误:

    function arraySum(arr: readonly number[]) {
      let sum = 0, num;
      while ((num = arr.pop()) !== undefined) {
        // ~~~ 'pop' does not exist on type 'readonly number[]'
        sum += num;
      return sum;
    

    值得注意的是 readonly 的限制是浅层的(shallow),下面的代码中,readonly 只能限制修改数组不能限制修改元素:

    const dates: readonly Date[] = [new Date()];
    dates.push(new Date());
    // ~~~~ Property 'push' does not exist on type 'readonly Date[]'
    dates[0].setFullYear(2037); // OK

    这种限制同样适用于 Readonly 泛型:

    interface Outer {
      inner: {
        x: number;
    const o: Readonly<Outer> = { inner: { x: 0 } };
    o.inner = { x: 1 };
    // ~~~~ Cannot assign to 'inner' because it is a read-only property
    o.inner.x = 1; // OK

    第 18 条 使用映射类型来保持值同步

    假设现在要实现一个散点图组件,它有着如下属性:

    interface ScatterProps {
      // The data
      xs: number[];
      ys: number[];
      // Display
      xRange: [number, number];
      yRange: [number, number];
      color: string;
      // Events
      onClick: (x: number, y: number, index: number) => void;
    

    为了优化性能,同时实现了一个 shouldUpdate 函数用于减少不必要的重绘,例如:

    function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
      let k: keyof ScatterProps;
      for (k in oldProps) {
        if (oldProps[k] !== newProps[k]) {
          if (k !== 'onClick') return true;
      return false;
    
    function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
      return (
        oldProps.xs !== newProps.xs ||
        oldProps.ys !== newProps.ys ||
        oldProps.xRange !== newProps.xRange ||
        oldProps.yRange !== newProps.yRange ||
        oldProps.color !== newProps.color
        // (no check for onClick)
    

    前者判断 onClick,后者判断 onClick 之外的剩余属性。如果给散点图增加了新的属性,前者即使是事件监听器绑定、解绑也会导致重绘,后者无论什么新属性变更都会忽略。总之如果添加了新属性,必须记得维护 shouldUpdate 的判断条件,不维护不会被察觉。

    我们可以借助映射类型(Mapped Types)来解决这个两难的问题:

    const REQUIRES_UPDATE: { [k in keyof ScatterProps]: boolean } = {
      xs: true,
      ys: true,
      xRange: true,
      yRange: true,
      color: true,
      onClick: false,
    function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
      let k: keyof ScatterProps;
      for (k in oldProps) {
        if (oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]) {
          return true;
      return false;
    

    如果 ScatterProps 增加了一个新属性,但没有同步修改 REQUIRES_UPDATE,会直接报错:

    interface ScatterProps {
      // ...