如何在函数中的记录中使用联合类型作为通用参数

How to use union type as a generic parameter in a record which in a function

我对 Record 中的通用联合类型感到困惑。

这是我的代码(TypeScript 版本:4.6.3):

   const fn = <T extends number | string = string>() => {
      const map: Record<T, string> = {};
   };

我希望它能起作用,但编辑器显示 map:

错误

'map' is declared but its value is never read.ts(6133);

Type '{}' is not assignable to type 'Record<T, string>'.ts(2322)

当我这样写的时候,它起作用了:

  type GetRecord<T extends string | number> = Record<T, string>;
  type AAAA = GetRecord<string | number>;
  const map: AAAA = {};

那么,如果我想在函数中的 record which 中使用联合类型作为泛型参数,我该怎么办?

而且,如果我这样写:

  const fn = <T extends number | string = string>() => {
    const map: Record<T, string> = {} as Record<T, string>;
    const data: Record<string, string> = {};
    Object.keys(data).forEach((key) => {
      map[key] = data[key];
    });
  };

显示错误:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Record<T, string>'.

No index signature with a parameter of type 'string' was found on type 'Record<T, string>'.

我不明白,T是extends string或number,key是string类型。为什么我不能将键设置为要映射的对象键?

因为extends 检查左轴是否可以分配给右轴。让我们使用 extends 编写一些测试以更好地了解其行为方式:

type T1 = {} extends Record<number | string, string> ? true : false;
//   ^? true
type T2 = {} extends Record<string, string> ? true : false;
//   ^? true
type T3 = {} extends Record<number, string> ? true : false;
//   ^? true

所有这些都按预期工作,但请注意当我们使用文字代替时:

type T4 = {} extends Record<1 | 2 | 3, string> ? true : false;
//   ^? false

不行!为什么?

嗯,Record<1 | 2 | 3, string>实际上是:

{
    1: string;
    2: string;
    3: string;
}

现在您看到 {} 不可分配给此类型。但是,我们为什么要首先尝试查看文字是否可行?

这些文字 (1, 2, 3) 是更一般的 number 类型的 子类型 string.

类型的文字“foo”、“bar”和“baz”也是如此

代码中的赋值不起作用,因为 T 可以是 任何字符串或数字类型,包括文字 。换句话说,TypeScript 不确定 T 不能包含文字类型。因此,TypeScript 不允许您将 {} 分配给 Record<T, string>.

你可以通过强制转换来解决这个问题:

const map = {} as Record<T, string>;

playground 通过强制转换演示了上述测试和变通解决方案。


(根据问题编辑更新)

让我们尝试从 TypeScript 解密此错误消息:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Record<T, string>'.

No index signature with a parameter of type 'string' was found on type 'Record<T, string>'.

确实很奇怪。直到你考虑并记住上面的测试。我们看到文字不起作用,因为它们太具体了。

所以让我们用 extends 编写更多测试:

type T6 = 1 | 2 | 3 extends number ? true : false;
//   ^? true
type T7 = "a" | "b" extends string ? true : false;
//   ^? true
type T8 = number extends 1 | 2 | 3 ? true : false;
//   ^? false
type T9 = string extends "a" | "b" ? true : false;
//   ^? false

现在很明显,文字可以分配给它们各自更通用的父类型,但反之则不行。这是直觉和预期的。您不能给期望 1、2 或 3 的任何数字,也不能将任何字符串仅分配给“a”或“b”。

那么这告诉我们什么?

提示在错误信息的最后一行:

No index signature with a parameter of type 'string' was found on type 'Record<T, string>'.

回想一下 T 可以是 string | number 的任何扩展。这包括 1 | 2 | 3"a" | "b" 甚至 1 | 2 | 3 | "a" | "b".

现在很清楚,因为 T 可能比普通 string 更具体,你不能使用普通 string 索引到 Record<T, string>

但是,如果您确定密钥可以与 T 一起使用,还可以使用另一个转换:

map[key as T] = data[key];

另一个 playground 演示了我们刚刚发现的内容。