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

Posted

技术标签:

【中文标题】Type[keyof Type] 函数参数的 Typescript 类型保护【英文标题】:Typescript type guard for Type[keyof Type] function parameter 【发布时间】:2021-09-12 19:32:06 【问题描述】:

抱歉标题混乱。

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

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

例子:

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

TS Playground

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

【问题讨论】:

你想达到什么目的?变异函数参数,被认为是一种不好的做法 @captain-yossarian 我同意。我将编辑问题以反映我的意图。我想使用我知道的类型来使用参数。 【参考方案1】:

第一个问题是编译器不能通过在handleProperty()的实现中检查k的值来缩小类型参数K。 (见microsoft/TypeScript#24085。)它甚至没有尝试。从技术上讲,编译器不这样做是正确的,因为K extends "name" | "age" 并不意味着K"name""age"。它可能是完整的联合"name" | "age",在这种情况下,您不能假设检查kK 有暗示,因此对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

【讨论】:

感谢您提供极其详细和翔实的解释!添加了对相关联合类型问题的赞许。

以上是关于Type[keyof Type] 函数参数的 Typescript 类型保护的主要内容,如果未能解决你的问题,请参考以下文章

在 Immutable.js 中,类型“字符串”不可分配给 keyof

round方法

参数验证

python isinstance()函数和type()函数

R语言使用plot函数和lines函数可视化线图(line plot)时图之间的主要区别是由选项type产生的type参数常用参数说明不同type生成的可视化图像对比

将文件中的键/值参数读入 shell 脚本