函数参数上的嵌套泛型的类型推断?

huangapple go评论62阅读模式
英文:

Type inference on function parameters with nested generics?

问题

我面临了与TypeScript泛型相关的问题。

我有一个必须根据类型和一些依赖于此类型的属性来调用的函数。
当在条件语句中对类型进行保护以匹配 TYPES.ME 时,TypeScript 无法推断属性的类型:

enum TYPES {
    ME = 'me',
    YOU = 'you'
}

type Attributes<T extends TYPES> =  T extends TYPES.ME ? {keys: true} : {hat: true}
const func = <T extends TYPES>(type: T, attr: Attributes<T>) => {
    if(type === TYPES.ME) {
        attr.keys // 错误:属性 'keys' 不存在于类型 '{ keys: true; } | { hat: true; }'
    }
} 

Playground链接

我想听听您对这个问题的意见,看看是否有一个好的解决方法。

祝好!

英文:

I am facing an issue with Typescript generics.

I have a function which must be called with a type and some attributes depending on this type.
Typescript does not manage to infer the type of attributes when inside an if guarding the type to TYPES.ME:

enum TYPES {
    ME = &#39;me&#39;,
    YOU = &#39;you&#39;
}

type Attributes&lt;T extends TYPES&gt; =  T extends TYPES.ME ? {keys: true} : {hat: true}
const func = &lt;T extends TYPES&gt;(type: T, attr: Attributes&lt;T&gt;) =&gt; {
    if(type === TYPES.ME) {
        attr.keys // error Property &#39;keys&#39; does not exist on type &#39;{ keys: true; } | { hat: true; }&#39;
    }
} 

Playground link

I would like to have your opinions on this matter and see if there is a nice workaround.

Cheers!

答案1

得分: 1

目前,TypeScript 无法根据控制流分析重新约束 泛型 类型参数。在 func(type, attr) 的函数体内,您检查 type === Types.ME。这可以将 type 的类型从 T 缩小到类似于 T & Types.ME。**但它无法改变 T 本身。**类型参数 T 仍然保持不变;它未被约束为 Types.ME。因此编译器无法得出 attr 的类型是 Attributes&lt;Types.ME&gt;

从技术上讲,编译器拒绝更改 T 是正确的。这是因为,虽然像 type 这样的个别值一次只能是一种类型,但像 T 这样的类型参数可以是 联合类型。事实上,您可以使用等于完整 Types.ME | Types.YOU 联合的 T 调用 func(),就像这样:

func(
    Math.random() &lt; 0.999 ? Types.ME : Types.YOU,
    { hat: true }
) // 没有错误编译

如果您检查这个调用,您将看到 T 被推断为 TypesTypes.ME | Types.YOU 的完整联合),因此即使在 typeTypes.ME 的情况下,也允许 attr{hat: true},即使这种情况非常可能。

有一个长期存在的功能请求在 microsoft/TypeScript#27808 中提到,请求一种方式来表示 "T 将确切地是 Types.METypes.YOU 之一;它不能是一个联合"。然后,也许在函数体内,检查 type === Types.ME 将允许将 T 本身约束为 Types.ME,并且一切都会按预期工作。预计使用 Math.random() &lt; 0.999 的调用将被拒绝。

但目前这不是该语言的一部分。


您可以考虑采用一种不同的方法,而不是使 func 成为泛型,可以将其类似于 重载函数 的函数,其中每个 Types 成员都有一个调用签名。您可以编写一个带有 剩余参数 类型为 辨识联合元组类型 的函数,并且编译器将在函数体内将其视为这样处理。

也许像这样:

type FuncArg =
    [type: Types.ME, attr: { keys: true }] |
    [type: Types.YOU, attr: { hat: true }];

const func: (...args: FuncArg) => void = (type, attr) => {
    if (type === Types.ME) {
        console.log(attr.keys) // 这可以工作
    }
}

现在,您无法进行无效调用:

func(
    Math.random() &lt; 0.999 ? Types.ME : Types.YOU,
    { hat: true }
) // 错误,类型 'Types.ME' 无法分配给类型 'Types.YOU'

一切都正常工作,因为现在 typeattr 被绑定在 FuncArg 中,就像想象 T 被约束为 Types 中的一个成员一样,并且可以遍历可能性。注意,如果需要,FuncArg 可以从问题中给出的 Attributes 类型或其他映射接口计算,但这超出了问题的范围。

英文:

Currently, TypeScript is unable to re-constrain generic type parameters as a result of control flow analysis. Inside the body of func(type, attr), you check that type === Types.ME. This can narrow the type of type from T to something like T &amp; Types.ME. But it cannot do anything to T itself. The type parameter T stubbornly stays the same; it is not constrained to Types.ME. And thus the compiler cannot conclude that attr is of type Attributes&lt;Types.ME&gt;.

And it is technically correct for the compiler to refuse to change T. That's because, while individual values like type can only be one thing at a time, a type argument like T can be a union. Indeed, you can call func() with a T equal to the full Types.ME | Types.YOU union, like so:

func(
    Math.random() &lt; 0.999 ? Types.ME : Types.YOU,
    { hat: true }
) // compiles without error

If you inspect that, you'll see that T is inferred as Types (the full union of Types.ME | Types.YOU, and therefore attr is allowed to be {hat: true} even in the 99.9% likely event that type is Types.ME.

There is a longstanding open feature request at microsoft/TypeScript#27808 which asks for a way to say "T will be exactly one of Types.ME or Types.YOU; it cannot be a union". And then, maybe inside the function body, checking type === Types.ME would allow T itself to be constrained to Types.ME, and things would work as expected. And presumably the call with Math.random() &lt; 0.999 would be rejected.

But for now it's not part of the language.


You might consider taking the approach where instead of having func be generic, you make it similar to an overloaded function, where it has one call signature per member of Types. You can write that as a function with a rest parameter whose type is a discriminated union of tuple types, and the compiler will treat it as such inside the function body.

Perhaps like this:

type FuncArg =
    [type: Types.ME, attr: { keys: true }] |
    [type: Types.YOU, attr: { hat: true }];

const func: (...args: FuncArg) =&gt; void = (type, attr) =&gt; {
    if (type === Types.ME) {
        console.log(attr.keys) // this works
    }
}

And now you can't make the invalid call:

func(
    Math.random() &lt; 0.999 ? Types.ME : Types.YOU,
    { hat: true }
) // error, type &#39;Types.ME&#39; is not assignable to type &#39;Types.YOU&#39;

Everything works because now type and attr are bound together as desired in FuncArg; it's like imagining T were constrained to be just one member of Types at a time, and walking through the possibilities. Note that FuncArg could, if necessary, be computed from the Attributes type given in the question, or from another mapping interface, but that is out of scope for the question as asked.

Playground link to code

huangapple
  • 本文由 发表于 2023年6月1日 22:36:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/76383035.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定