第一个问题是编译器不能通过在handleProperty()的实现中检查k的值来缩小类型参数K。 (见microsoft/TypeScript#24085。)它甚至没有尝试。从技术上讲,编译器不这样做是正确的,因为K extends "name" | "age" 并不意味着K 是"name" 或"age"。它可能是完整的联合"name" | "age",在这种情况下,您不能假设检查k 对K 有暗示,因此对T[K] 有暗示:
handleProperty(x, Math.random() < 0.5 ? "name" : "age", "bar"); // accepted!
在这里您可以看到k 参数的类型为"name" | "age",这就是K 的推断类型。因此v 参数被允许为string | number 类型。所以暗示中的错误是正确的:k 可能是"age" 和v 可能仍然是string。这完全违背了您的功能的目的,绝对不是您的预期用例,但编译器担心这是一种可能性。
您真正想说的是要么 K extends "name" 或 K extends "age",或类似K extends_one_of ("name", "age"),(见microsoft/TypeScript#27808,)但目前没有办法表示这一点。因此,泛型并不能真正为您提供您想要转向的手柄。
当然,您不必担心有人使用完整联合调用handleProperty(),但您需要在实现中使用type assertion,例如v as number。
如果您想将调用者实际约束到预期的用例,您可以使用 rest tuples 的联合而不是泛型:
type KV = { [K in keyof Entity]: [k: K, v: Entity[K]] }[keyof Entity]
// type KV = [k: "name", v: string] | [k: "age", v: number];
function handleProperty(e: Entity, ...[k, v]: KV): void {
// impl
}
handleProperty(x, 'name', 10); // Error
handleProperty(x, 'name', 'bar'); // Good
handleProperty(x, 'age', 'bar'); // Error
handleProperty(x, 'age', 20); // Good
handleProperty(x, Math.random() < 0.5 ? "name" : "age", "bar"); // Error
您可以看到KV 类型是元组的联合(由mapping Entity 创建到其属性是此类元组的类型,然后立即looking up 这些属性的联合)并且@987654359 @ 接受它作为最后两个参数。
很好,对吧?不幸的是,这并不能解决实现内部的问题:
function handleProperty(e: Entity, ...[k, v]: KV): void {
if (k === 'age') {
console.log(v + 2); // still error!
}
console.log(v);
}
这是由于缺乏对我一直称为相关联合类型的支持(请参阅microsoft/TypeScript#30581)。编译器将解构的k 的类型视为"name" | "age",将解构的v 的类型视为string | number。这些类型是正确的,但不是全部。通过解构 rest 参数,编译器忘记了第一个元素的类型与第二个元素的类型相关。
所以,要绕过那个,你不能解构 rest 参数,或者至少在你检查它的第一个元素之前不能。例如:
function handleProperty(e: Entity, ...kv: KV): void {
if (kv[0] === 'age') {
console.log(kv[1] + 2) // no error, finally!
// if you want k and v separate
const [k, v] = kv;
console.log(v + 2) // also no error
}
console.log(kv[1]);
}
在这里,我们将其余元组保留为单个数组值kv。编译器将此视为discriminated union,当您检查kv[0](前k)时,编译器将最终为您缩小kv的类型,以便kv[1]也会变窄。使用kv[0] 和kv[1] 很难看,虽然您可以在检查kv[0] 后通过解构来部分缓解这种情况,但它仍然不是很好。
这样,handleProperty() 的完全类型安全(或至少更接近类型安全)实现就完成了。这值得么?可能不是。在实践中,我发现编写惯用的 JavaScript 和类型断言来消除编译器警告通常会更好,就像你一开始所做的那样。
Playground link to code