简体   繁体   English

Typescript 类型保护用于 Type[keyof Type] function 参数

[英]Typescript type guard for Type[keyof Type] function parameter

Sorry for the confusing title.对不起,令人困惑的标题。

I'm trying to use a lookup type similar to the setProperty example in https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types我正在尝试使用类似于https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types中的setProperty示例的查找类型

The lookup type is correctly checked when calling the function but can't be used inside the function.调用 function 时已正确检查查找类型,但不能在 function 内部使用。 I tried working around this with a type guard, but this doesn't seem to work.我尝试使用类型保护解决此问题,但这似乎不起作用。

Example:例子:

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 TS游乐场

Is there any way to make typescript figure this out, without hard-coding a type assertion: (v as number) ?有没有办法让 typescript 弄清楚这一点,而无需对类型断言进行硬编码: (v as number) At that point in the code, the compiler should be able to infer that v is a number.在代码中,编译器应该能够推断出v是一个数字。

The first problem is that the compiler cannot narrow the type parameter K by checking the value of k inside the implementation of handleProperty() .第一个问题是编译器无法通过在handleProperty()的实现中检查k的值来缩小类型参数K (See microsoft/TypeScript#24085 .) It doesn't even try. (参见microsoft/TypeScript#24085 。)它甚至没有尝试。 Technically, the compiler is correct not to do so, because K extends "name" | "age"从技术上讲,编译器不这样做是正确的,因为K extends "name" | "age" K extends "name" | "age" does not mean that K is either "name" or "age" . K extends "name" | "age"并不意味着K"name""age" It could be the full union "name" | "age"可能是完整的工会"name" | "age" "name" | "age" , in which case, you cannot assume that checking k has an implication for K and thus T[K] : "name" | "age" ,在这种情况下,您不能假设检查kK有影响,因此对T[K]有影响:

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

Here you can see that the k parameter is of type "name" | "age"在这里你可以看到k参数的类型是"name" | "age" "name" | "age" , and so that's what K is inferred to be. "name" | "age" ,所以这就是K的推断。 Thus the v parameter is allowed to be of type string | number因此, v参数允许为string | number类型。 string | number . string | number So the error inside the implication is correct: k might be "age" and v might still be a string .所以暗示中的错误是正确的: k可能是"age"v可能仍然是string This completely defeats the purpose of your function and is definitely not your intended use case, but it's a possibility the compiler is worried about.这完全违背了您的 function 的目的,并且绝对不是您的预期用例,但编译器可能会担心。

Really what you'd like to say is that either K extends "name" or K extends "age" , or something like K extends_one_of ("name", "age") , (see microsoft/TypeScript#27808 ,) but there is currently no way to represent that.你真的想说的是,要么K extends "name"要么K extends "age" ,或者像K extends_one_of ("name", "age") ,(见microsoft/TypeScript#27808 ,)但是有目前没有办法表示。 So generics don't really give you the handle you're trying to turn.所以 generics 并没有真正给你你想要转动的把手。

Of course you could just not worry about someone calling handleProperty() with the full union, but you'll need a type assertion inside the implementation like v as number .当然,您不必担心有人使用完整的联合调用handleProperty() ,但是您需要在实现中使用类型断言,例如v as number


If you want to actually constrain callers to the intended use cases, you can use a union of rest tuples instead of generics:如果您想实际将调用者限制在预期的用例中,您可以使用rest 元组的联合,而不是 generics:

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

You can see that the type KV is a union of tuples (created by mapping Entity to a type whose properties are such tuples and then immediately looking up the union of those properties) and that handleProperty() accepts that as its last two arguments.您可以看到类型KV是元组的并集(通过将Entity 映射到其属性为此类元组的类型然后立即查找这些属性的并集来创建),并且handleProperty()将其作为最后两个 arguments 接受。

Great, right?太好了,对吧? Well unfortunately that does not solve the problem inside the implementation:不幸的是,这并不能解决实现内部的问题:

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

This is due to lack of support for what I've been calling correlated union types (see microsoft/TypeScript#30581 ).这是由于缺乏对我所称的相关联合类型的支持(参见microsoft/TypeScript#30581 )。 The compiler sees the type of the destructured k as "name" | "age"编译器将解构后k的类型视为"name" | "age" "name" | "age" and the type of the destructured v as string | number "name" | "age"和解构的v的类型为string | number string | number . string | number Those types are correct, but are not the full story.这些类型是正确的,但不是全部。 By destructuring the rest argument, the compiler has forgotten that the type of the first element is correlated to the type of the second element.通过解构 rest 参数,编译器忘记了第一个元素的类型与第二个元素的类型相关


So, to get around that , you can just not destructure the rest argument, or at least not until you check its first element.所以,为了解决这个问题,你不能解构 rest 参数,或者至少在你检查它的第一个元素之前不要解构。 For example:例如:

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]);
}

Here we are leaving the rest tuple as a single array value kv .在这里,我们将 rest 元组保留为单个数组值kv The compiler sees this as a discriminated union and when you check kv[0] (the former k ) the compiler will, finally , narrow the type of kv for you so that kv[1] will also be narrowed.编译器将此视为一个有区别的联合,当您检查kv[0] (前k )时,编译器最终将为您缩小kv的类型,以便kv[1]也将被缩小。 It's ugly using kv[0] and kv[1] , and while you could partially mitigate this by destructuring after the check of kv[0] , it's still not great.使用kv[0]kv[1]很难看,虽然您可以通过在检查kv[0]后解构来部分缓解这种情况,但它仍然不是很好。


So there you are, a fully type safe (or at least closer to type safe) implementation of handleProperty() .这样就完成了一个完全类型安全(或至少更接近于类型安全)的handleProperty()实现。 Is it worth it?这值得么? Probably not.可能不是。 In practice I find that it's usually just better to write idiomatic JavaScript along with a type assertion to quiet the compiler warnings, like you've done in the first place.在实践中,我发现编写惯用的 JavaScript 以及类型断言来消除编译器警告通常会更好,就像你一开始所做的那样。

Playground link to code Playground 代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM