[译]<<Effective TypeScript>> 技巧40:将不安全的断言藏在类型安全的函数

164 阅读2分钟

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

技巧40:将不安全的断言藏在类型安全的函数

有些函数类型声明很简单,但是用类型安全的方法实现却很难。这个时候很多人可能会考虑使用类型断言。当不得不用类型断言的时候,与其让断言到处散落,最好是让将不安全的断言藏在类型安全的函数。

假定你想缓存函数最后的调用结果,这是一个常用的技术用于react,用于节省函数频繁调用的资源。我们可以写一个 cacheLast 高阶函数能给函数添加缓存行为:

declare function cacheLast<T extends Function>(fn: T): T;

这是其中的一个实现方法:

function cacheLast<T extends Function>(fn: T): T {
  let lastArgs: any[]|null = null;
  let lastResult: any;
return function(...args: any[]) {
      // ~~~~~~~~~~~~~~~~~~~~~~~~~~
      //          Type '(...args: any[]) => any' is not assignable to type 'T'
    if (!lastArgs || !shallowEqual(lastArgs, args)) {
      lastResult = fn(...args);
      lastArgs = args;
    }
    return lastResult;
  };
}

这个报错很合理:因为ts无法相信返回的函数和T有什么关系。但是args传入会通过正确的类型检查,返回值lastResult也类型正确。我们在这添加断言没有危害。

如果我们这样添加断言:

function cacheLast<T extends Function>(fn: T): T {
  let lastArgs: any[]|null = null;
  let lastResult: any;
  return function(...args: any[]) {
if (!lastArgs || !shallowEqual(lastArgs, args)) {
      lastResult = fn(...args);
      lastArgs = args;
    }
    return lastResult;
  } as unknown as T;
}

这样能通过检查,但是隐藏了很多类型。有个问题:

不会检查连续调用的几个值是否相同,如果原始函数有一些定义好的属性,那么包装函数就不会有这些属性。因此就没有相同类型。

我们可以对shallowEqual函数做一下修改,如果shallowEqual比较的是两个array很简单。我们这里假定比较两个object。:

declare function shallowObjectEqual<T extends object>(a: T, b: T): boolean;

实现如下:

function shallowObjectEqual<T extends object>(a: T, b: T): boolean {
  for (const [k, aVal] of Object.entries(a)) {
    if (!(k in b) || aVal !== b[k]) {
                           // ~~~~ Element implicitly has an 'any' type
                           //      because type '{}' has no index signature
      return false;
    }
  }
  return 
Object.keys(a).length === Object.keys(b).length;
}

这个错误让人有点难以理解,因为你明明已经检查了 k in b, 但是还是报错: b[k] 无法获取。所以只能这样做:

function shallowObjectEqual<T extends object>(a: T, b: T): boolean {
  for (const [k, aVal] of Object.entries(a)) {
    if (!(k in b) || aVal !== (b as any)[k]) {
      return false;
    }
  }
  return Object.keys(a).length === Object.keys(b).length;
}

这样的断言没有危害,因为你检查了 k in b。函数的类型声明也正确。