如何修复类型 'string' 无法分配给类型 'T[keyof T]'

How to fix Type 'string' is not assignable to type 'T[keyof T]'

给定以下 Typescript 函数:

const setter = <T = Record<string, string>>(obj: T, prop: keyof T, val: string): void => {
    obj[prop] = val;
};

我的 IDE 出现以下错误:

Type 'string' is not assignable to type 'T[keyof T]'

如果T是一个字符串键和字符串值的Record,prop是T的一个键,那么我本来以为可以给T[keyof T]赋一个字符串。

如何正确使用泛型来修复此错误?

的直接问题
const setter = <T = Record<string, string>>(
  obj: T, prop: keyof T, val: string
): void => {
  obj[prop] = val; // error! 'string' is not assignable to 'T[keyof T]'
};

是编译器无法为 T 推断任何内容的(不太可能)事件中的 generic type parameter T can be absolutely anything the caller of the function wants it to be. All you've done with T = Record<string, string> is specified that T would defaultRecord<string, string>。但是 T 将由传入的任何值推断为 obj,如果调用者未手动指定:

setter({ a: 1 }, "a", "oops"); // no compiler error

所以这不是你想说的。


也许您试图 constrain TRecord<string, string> 而不是默认设置。用 extends 表示,而不是 =:

const setter = <T extends Record<string, string>>(
  obj: T, prop: keyof T, val: string
): void => {
  obj[prop] = val; // Type 'string' is not assignable to type 'T[keyof T]'
};

这至少可以防止人们在 obj 在相关键上有一个非 string 属性 的情况下进行完全错误的调用::

setter({ a: 1 }, "a", "oops"); // error!
// ----> ~ // number is not assignable to string

但是像这样的约束的问题是调用者可以 属性缩小到比 string 更具体的东西,例如 union of string literal types:

type Foo = { b: "x" | "y" };
const foo: Foo = { b: "x" };
badSetter2(foo, "b", "oopsie"); // no compiler error

这里,fooFoo 类型的对象类型,其 b 属性 必须是 "x""y"。我们显然已经将 "oopsie" 分配给它的 b 属性,这是不允许的。

对于您的预期用例,这种缩小可能不太可能发生,但编译器会关注它,因此错误仍然存​​在。您不一定可以将 string 分配给 obj[prop]


解决此问题的一种方法是确保 val 的类型可以分配给 prop 处的特定键。这涉及为键添加类型参数:

const setter = <T extends Record<string, string>, K extends keyof T>(
  obj: T, prop: K, val: T[K]
): void => {
  obj[prop] = val; // okay
};

setter({ a: 1 }, "a", "oops"); // compiler error
setter(foo, "b", "oopsie"); // compiler error
setter({ z: "hello" }, "z", "goodbye"); // okay

现在一切正常。将 prop 作为类型 K,将 val 作为类型 T[K](当你 index into 类型为 T 的对象时得到的类型K 类型的键),则 val 可被视为可分配给 obj[prop]。并且正确地拒绝了从上面对 setter() 的无效调用。

这可能是执行此操作的“正确”方法。


您可以通过完全删除 T 来稍微简化它(以牺牲一点正确性为代价)。您只关心 objprop 属性 处有一个 string 值键,然后您可以离开 K 并改写如下:

const setter = <K extends PropertyKey>(
  obj: Record<K, string>, prop: K, val: string
): void => {
  obj[prop] = val; // okay
};

setter({ a: 1 }, "a", "oops"); // compiler error
setter(foo, "b", "oopsie"); // okay?!
setter({ z: "hello" }, "z", "goodbye"); // okay

这再次防止了疯狂调用,但仍然允许错误调用,例如将 foo.b 设置为 "oopsie"


最后,您可以进一步简化它;如果您甚至不关心特定的键类型 K,您可以将其设为 non-generic 函数:

const setter = (
  obj: Record<string, string>, prop: string, val: string
): void => {
  obj[prop] = val;
};

setter({ a: 1 }, "a", "oops"); // compiler error
setter(foo, "b", "oopsie"); // okay?!
setter({ z: "hello" }, "z", "goodbye"); // okay

这在假阴性方面并不比前一个差,但可能有更多的假阳性,我不会深入讨论,因为这个答案已经太长了。


好了。您有一系列解决方案可供选择,从完全通用到完全 non-generic,根据您的用例,其中任何一个都可能或多或少有用。

Playground link to code