Type[keyof Type] 函数参数的 Typescript 类型保护

wen*_*een 2 typescript typescript-generics

抱歉标题令人困惑。

我正在尝试使用类似于https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-typessetProperty中的示例的查找类型

调用函数时会正确检查查找类型,但不能在函数内部使用。我尝试使用类型保护来解决这个问题,但这似乎不起作用。

例子:

interface Entity {
  name: string;
  age: number;
}

function handleProperty<K extends keyof Entity>(e: Entity, k: K, v: Entity[K]): void {
  if (k === 'age') {
    //console.log(v + 2); // Error, v is not asserted as number
    console.log((v as number) + 2); // Good
  }
  console.log(v);
}

let x: Entity = {name: 'foo', age: 10 }

//handleProperty(x, 'name', 10); // Error
handleProperty(x, 'name', 'bar'); // Good
// handleProperty(x, 'age', 'bar'); // Error
handleProperty(x, 'age', 20); // Good

Run Code Online (Sandbox Code Playgroud)

TS游乐场

有没有什么方法可以让打字稿解决这个问题,而无需硬编码类型断言:(v as number)?在代码中的这一点上,编译器应该能够推断出这v是一个数字。

jca*_*alz 5

第一个问题是编译器无法通过检查 的实现内部的K值来缩小类型参数的范围。(请参阅microsoft/TypeScript#24085。)它甚至没有尝试。从技术上讲,编译器不这样做是正确的,因为并不意味着要么要么。它可能是完整的 union ,在这种情况下,您不能假设检查对and 因此有影响:khandleProperty()K extends "name" | "age"K"name""age""name" | "age"kKT[K]

handleProperty(x, Math.random() < 0.5 ? "name" : "age", "bar"); // accepted!
Run Code Online (Sandbox Code Playgroud)

在这里您可以看到该k参数的类型为"name" | "age",因此这就是K推断的内容。因此,该v参数允许为 类型string | number。因此,蕴涵中的错误是正确的:k可能是"age"并且v可能仍然是 a string。这完全违背了函数的目的,并且绝对不是您预期的用例,但这是编译器担心的可能性。

实际上,您想说的是 K extends "name" K extends "age"或类似K extends_one_of ("name", "age"), (请参阅microsoft/TypeScript#27808,),但目前无法表示这一点。因此,仿制药并不能真正为您提供您想要控制的控制权。

当然,您可以不必担心有人使用handleProperty()完整的联合进行调用,但是您需要在实现中进行类型断言,v as number例如.


如果您想实际将调用者限制到预期的用例,您可以使用剩余元组的联合而不是泛型:

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

您可以看到该类型KV是元组的并集(通过映射 Entity到其属性是此类元组的类型然后立即查找这些属性的并集来创建)并且handleProperty()接受它作为其最后两个参数。

太棒了,对吧?不幸的是,这并不能解决实现中的问题:

function handleProperty(e: Entity, ...[k, v]: KV): void {
  if (k === 'age') {
    console.log(v + 2); // still error!
  }
  console.log(v);
}
Run Code Online (Sandbox Code Playgroud)

这是由于缺乏对我所说的相关联合类型的支持(请参阅microsoft/TypeScript#30581)。编译器将解构后的类型视为k"name" | "age"将解构后的类型v视为string | number。这些类型是正确的,但并不是故事的全部。通过解构剩余参数,编译器忘记了第一个元素的类型与第二个元素的类型相关。


因此,为了解决这个问题,您可以不解构其余参数,或者至少在检查其第一个元素之前不解构。例如:

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

在这里,我们将其余元组保留为单个数组值kv。编译器将其视为可区分的联合,当您检查kv[0](前者k)时,编译器最终kv将为您缩小 的类型,以便kv[1]也缩小范围。kv[0]使用and是很丑陋的kv[1],虽然你可以通过在检查之后解构来部分缓解这个问题kv[0],但它仍然不是很好。


这样,您就得到了一个完全类型安全(或至少接近类型安全)的handleProperty(). 这值得么?可能不会。在实践中,我发现通常最好编写惯用的 JavaScript 和类型断言来消除编译器警告,就像您一开始所做的那样。

Playground 代码链接