Type[keyof Type] 函数参数的 Typescript 类型保护 [英] Typescript type guard for Type[keyof Type] function parameter

查看:29
本文介绍了Type[keyof Type] 函数参数的 Typescript 类型保护的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

抱歉标题混乱.

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

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

示例:

接口实体{名称:字符串;年龄:数量;}function handleProperty<K extends keyof Entity>(e: Entity, k: K, v: Entity[K]): void {如果(k === '年龄'){//控制台.log(v + 2);//错误,v 没有被断言为数字console.log((v as number) + 2);//好的}控制台日志(v);}让 x: Entity = {name: 'foo', age: 10 }//handleProperty(x, 'name', 10);//错误handleProperty(x, 'name', 'bar');//好的//handleProperty(x, 'age', 'bar');//错误handleProperty(x, '年龄', 20);//好的

TS游乐场

有没有办法让打字稿弄清楚这一点,而无需对类型断言进行硬编码:(v as number)?在代码中的那个点,编译器应该能够推断出 v 是一个数字.

解决方案

第一个问题是编译器无法通过检查k的值来缩小类型参数KhandleProperty() 的实现中.(请参阅 microsoft/TypeScript#24085.)它甚至没有尝试.从技术上讲,编译器不这样做是正确的,因为 K 扩展了name".|age" 并不意味着 Kname"age".它可能是完整的联合name"|年龄",在这种情况下,您不能假设检查 kK 有影响,因此 T[K]:

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

在这里您可以看到 k 参数的类型为 name"|年龄",这就是推断 K 的原因.因此 v 参数可以是 string | 类型.号码.所以暗示中的错误是正确的:k 可能是 "age"v 可能仍然是一个 string.这完全违背了您的函数的目的,绝对不是您的预期用例,但编译器可能会担心.

你真正想说的是要么 K 扩展name" K 扩展age"; 或类似 K extends_one_of ("name", "age"),(请参阅 microsoft/TypeScript#27808,) 但目前无法表示.因此泛型并没有真正为您提供您想要转向的控制权.

当然,您不必担心有人使用完整的联合调用 handleProperty(),但是您需要一个 type assertion 在实现中,例如 v as number.


如果您想真正将调用者限制在预期的用例中,您可以使用 休息元组 而不是泛型:

type KV = { [K in keyof Entity]: [k: K, v: Entity[K]] }[keyof Entity]//类型 KV = [k: "name", v: string] |[k:年龄",v:数量];function handleProperty(e: Entity, ...[k, v]: KV): void {//实现}handleProperty(x, 'name', 10);//错误handleProperty(x, 'name', 'bar');//好的handleProperty(x, 'age', 'bar');//错误handleProperty(x, '年龄', 20);//好的handleProperty(x, Math.random() <0.5 ? name" : age", bar");//错误

您可以看到类型 KV 是元组的联合(由 映射 Entity 到属性是这样的元组的类型,然后立即查找这些属性的联合)和 handleProperty() 接受它作为最后两个参数.

很好,对吧?不幸的是,这并没有解决实现内部的问题:

function handleProperty(e: Entity, ...[k, v]: KV): void {如果(k === '年龄'){控制台日志(v + 2);//还是报错!}控制台日志(v);}

这是由于缺乏对我一直称之为相关联合类型的支持(请参阅microsoft/TypeScript#30581).编译器将解构后的 k 的类型视为 name".|age" 和解构后的 v 的类型为 string |号码.这些类型是正确的,但不是全部.通过解构 rest 参数,编译器忘记了第一个元素的类型与第二个元素的类型相关.


所以,为了解决那个,你不能解构rest参数,或者至少在你检查它的第一个元素之前不能解构.例如:

function handleProperty(e: Entity, ...kv: KV): void {如果(kv [0] === '年龄'){console.log(kv[1] + 2)//没有错误,终于!//如果你想要 k 和 v 分开const [k, v] = kv;console.log(v + 2)//也没有错误}控制台日志(kv [1]);}

这里我们将其余元组作为单个数组值kv.编译器将此视为一个歧视联合当你检查kv[0](以前的k)时,编译器会最后,缩小kv的类型code> 以便 kv[1] 也将被缩小.使用 kv[0]kv[1] 很丑,虽然你可以通过在检查 kv[0],还是不行.


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

参考代码rl=mECOH6np>参考代码

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

The lookup type is correctly checked when calling the function but can't be used inside the 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

Is there any way to make typescript figure this out, without hard-coding a type assertion: (v as number)? At that point in the code, the compiler should be able to infer that v is a number.

解决方案

The first problem is that the compiler cannot narrow the type parameter K by checking the value of k inside the implementation of handleProperty(). (See microsoft/TypeScript#24085.) It doesn't even try. Technically, the compiler is correct not to do so, because K extends "name" | "age" does not mean that K is either "name" or "age". It could be the full union "name" | "age", in which case, you cannot assume that checking k has an implication for K and thus 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", and so that's what K is inferred to be. Thus the v parameter is allowed to be of type string | number. So the error inside the implication is correct: k might be "age" and v might still be a 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.

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. So generics don't really give you the handle you're trying to turn.

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.


If you want to actually constrain callers to the intended use cases, you can use a union of rest tuples instead of 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.

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). The compiler sees the type of the destructured k as "name" | "age" and the type of the destructured v as 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.


So, to get around that, you can just not destructure the rest argument, or at least not until you check its first element. 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. 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. 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.


So there you are, a fully type safe (or at least closer to type safe) implementation of 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.

Playground link to code

这篇关于Type[keyof Type] 函数参数的 Typescript 类型保护的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆