为了使其工作,您需要在key 输入的类型中创建函数generic。让我们更改您的示例,使其仅适用于设置 User 的属性,并使用该函数创建该类型的 user 来修改:
const user: User = {
name: "",
age: 0,
friends: [],
hasPet: false
}
function setUserValue<K extends keyof User>(key: K, value: User[K]) {
user[key] = value;
}
在这里您可以看到该函数在K 中是通用的,从constrained 到keyof User。 key 输入是 K 类型,value 输入是 User[K] 类型,indexed access type 表示“User 的属性类型在 K 类型的键上”。让我们看看它是如何工作的:
setUserValue("age", true); // error, boolean isn't a number
setUserValue("age", 25) // okay
看起来不错。编译器将K 推断为literal type "age",然后希望value 的类型为User["age"],即number。
简单版的回答到此结束。
这是这个问题的标准解决方案;请注意,使用此功能仍然可以做不安全的事情,尽管可能性较小。例如,当key 属于union type 时,可能会出错:
const key = Math.random() < 0.5 ? "age" : "hasPet";
setUserValue(key, true); // no error! But this has a 50% chance of doing something bad
目前在 TypeScript 中没有直接的方式来说明我们希望 K 是 keyof User 联合中的完全一个元素,并且不允许它本身是联合类型。 microsoft/TypeScript#27808 有一个功能请求,要求对此提供支持。现在你需要接受key 成为一个联合体(或者以一种复杂的方式阻止它,我不会在这里讨论)。
真正的问题是索引访问类型T[K] 仅对读取 属性是安全的,因为T[K1 | K2] 变为T[K1] | T[K2]。但是对于writing,你真的希望T[K1 | K2] 被视为T[K1] & T[K2]...也就是说,键类型中的联合应该成为“写入索引访问”类型中的intersections。
你可以在 TypeScript 中表达这样的类型函数如下:
type WritingIdx<T, K extends keyof T> =
{ [P in K]: (x: T[P]) => void }[K] extends (x: infer I) => void ? I : never;
(这里我使用与here 基本相同的技术将联合变为交叉点)
然后你可以声明setUserValue() 使value 的类型为WritingIdx<User, K> 而不是User[K]:
function setUserValue<K extends keyof User>(key: K, value: WritingIdx<User, K>): void;
function setUserValue<K extends keyof User>(key: K, value: User[K]) {
user[key] = value;
}
请注意,我必须将其设为单呼签名overload,因为WritingIdx<User, K> 的一个缺点是当value 属于该类型时,编译器无法验证user[key] = value 是否安全。所以在实现中我将它扩大到User[K]。
我们来看看吧:
setUserValue("age", true); // error, boolean isn't a number
setUserValue("age", 25) // okay
setUserValue(key, true); //error!
如果我们想象User 有一些具有重叠类型的属性:
interface User {
name: string;
shoeSize: string | number;
}
这里shoeSize 可以是string 或number,但name 只能是string。那么他们俩上的setUserValue()应该只接受交集,也就是string:
const nameOrShoeSize = Math.random() < 0.5 ? "name" : "shoeSize"
setUserValue(nameOrShoeSize, "hasToBeAString"); // okay
setUserValue(nameOrShoeSize, 123); // error
那么,您需要这个更复杂的答案吗?可能不会,如果 key 参数只是一个字符串文字。即使有时会出现工会,你也可能不想把精力花在处理复杂而迂腐的代码库上。 TypeScript 一般allows unsafe things in the name of developer convenience,所以在类型系统中还有很多其他的“漏洞”。但我想我至少会谈谈与通用属性编写器函数有关的问题。
Playground link to code