如何修复类型“string”不可分配给类型“T[keyof T]”

Ang*_*eno 14 typescript typescript-generics

给定以下 Typescript 函数:

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

我从 IDE 中收到以下错误:

Type 'string' is not assignable to type 'T[keyof T]'
Run Code Online (Sandbox Code Playgroud)

如果 T 是字符串键和字符串值的记录,并且prop是 T 的键,那么我最初假设我可以将字符串分配给T[keyof T]。

正确使用泛型来修复此错误的方法是什么?

jca*_*alz 20

眼前的问题是

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]'
};
Run Code Online (Sandbox Code Playgroud)

是泛型类型参数T绝对可以是函数调用者想要的任何东西。您所做的一切T = Record<string, string>都是指定的,在T编译器无法推断. 但如果调用者未手动指定,则将通过传入的任何值来推断:Record<string, string>TTobj

setter({ a: 1 }, "a", "oops"); // no compiler error
Run Code Online (Sandbox Code Playgroud)

所以这不是你想说的。


也许你试图限制 T而不是默认Record<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]'
};
Run Code Online (Sandbox Code Playgroud)

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

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

但是像这样的约束的问题是调用者可以将属性缩小到比 更具体的东西string,例如字符串文字类型的联合:

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

这里,foo是 类型Foo,一个对象类型,其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
Run Code Online (Sandbox Code Playgroud)

现在一切都按预期进行。使用propas typeK和valas type T[K](当您索引到T具有类型键的类型的对象时获得的类型K),那么val可以被视为可分配给obj[prop]。setter()上面的无效调用被正确拒绝。

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


您可以通过完全删除来稍微简化它(以牺牲一点正确性为代价)T。您所关心的只是该房产obj有一把string有价值的钥匙prop,然后您可以离开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
Run Code Online (Sandbox Code Playgroud)

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


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

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
Run Code Online (Sandbox Code Playgroud)

就误报而言,这并不比前一个更糟糕,但可能会有更多误报,我不会讨论这一点,因为这个答案已经太长了。


所以就这样吧。您有一系列解决方案可供选择,从完全通用到完全非通用,其中任何一个都可能或多或少有用,具体取决于您的用例。

Playground 代码链接