网站搜索

typescript - 使用原类型的键索引映射类型


我正在编写一个解析 CSV 的函数,并返回一个类型化对象的数组。看起来是这样的。

type KeyToTransformerLookup<T> = {
  [K in keyof T as T[K] extends string ? never : K]: (
    stringValue: string
  ) => T[K];
};

export default function csvToArray<T>(
  csv: string,
  keys: Array<keyof T>,
  keyToTransformerLookup: KeyToTransformerLookup<T>,
  ignoreFirstLine = false
): T[] {
  const rows = csv.split(/\n(?=.)/);

  if (ignoreFirstLine) {
    rows.shift();
  }

  return rows.map((row) => {
    const values = row.split(/(?!\B"[^"]*),(?![^"]*"\B)/g);

    const rowObject: Record<any, any> = {};

    keys.forEach((key, index) => {
      const stringValue = values[index];

      if (key in keyToTransformerLookup) {
        rowObject[key] = keyToTransformerLookup[key](stringValue); // TypeScript complains
        return;
      }

      rowObject[key] = stringValue;
    });

    return rowObject as T;
  });
}
Type 'keyof T' cannot be used to index type 'KeyToTransformerLookup<T>'.

我猜这是因为一旦你映射 K in keyof T,生成的类型甚至不再是 keyof T 的部分类型,而是完全新的类型。这有点令人沮丧,因为我并没有转换这些键,而且我也在检查 key 是否存在于 keyToTransformerLookup,所以我只是想找到一种索引 keyToTransformerLookup 的方式,以满足 TypeScript。

答案#1

TypeScript 对通过 in 运算符进行类型缩小的支持(另见 TS4.9 对此的更新)目前只影响被检查的 对象 的表面类型,而不影响 键。也就是说,if (k in o) {⋯} else {⋯} 只对 o 有意义,而对 k 没有。目前在 microsoft/TypeScript#43284 上有一个开放的功能请求,建议也为 k 添加类型缩小支持,但截至 TS5.0,这还没有实现,并且实现起来可能会比较棘手,难以让所有人都满意。

所以当你检查 key in keyToTransformerLookup 时,key 的类型固执地保持为 keyof T,无法缩小为 keyof KeyToTransformerLookup<T>。


除非 TypeScript 添加对你尝试进行的那种类型缩小的支持,否则你将不得不想办法绕过它。 对于一次性的检查,最简单的解决方法是直接断言类型已经被缩小:

if (key in keyToTransformerLookup) {
  rowObject[key] = keyToTransformerLookup[
    key as keyof KeyToTransformerLookup<T>
  ](stringValue);
  return;
}

如果你发现自己经常进行这种检查,你可以为它编写一个自定义类型保护函数:

function isKeyof<T extends object>(
  k: PropertyKey, o: T): k is keyof T {
  return k in o;
}

然后使用它:

if (isKeyof(key, keyToTransformerLookup)) {
  rowObject[key] = keyToTransformerLookup[key](stringValue);
  return;
}

请注意,从技术上来说进行这种缩小类型是不安全的。TypeScript 中的对象类型是开放的或可扩展的,而不是封闭的或“精确”的(如 microsoft/TypeScript#12936 所讨论的)。对象总是可以拥有比其类型描述更多的属性。这意味着可能发生如下情况:

interface Foo {
  x: string;
  y: number;
}
const xformer = {
  y: (s: string) => +s,
  x: "oopsiedoodle" // <-- this is an excess property
}
csvToArray<Foo>("...", ["x", "y"], xformer); // no compiler error, but:
// 💥 RUNTIME ERROR! keyToTransformerLookup[key] is not a function

这可能不太可能,但你应该了解它。请参阅 extend 并仅指定已知属性?以获取更多信息。

代码的 Playground 链接