Typescript 无故回退到泛型类型的默认值

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

Typescript falls back to the default value of generic type for no reason

问题

抱歉,你的请求不太清楚,代码部分已被排除在翻译之外。你是否需要其他帮助?

英文:

Sorry if the title is not very obvious. I don't know what is the problem so I couldn't come up with a good question. This is a simplified version of the problem that I'm struggling with:

type Key = unknown[];

type Option<TKey extends Key> = {
  key: TKey;
  updateKey: (v: TKey) => TKey;
};

type Getter = <Value>(v: Value) => Value

function createConfigWithOption<TKey extends Key>(
  getOptions: ((g: Getter) => Option<TKey>)
) {
}

createConfigWithOption receives a function whose first parameter is a getter function (doesn't matter what it does) and returns an option object. The option object has a key key and an updateKey function. The input and output of updateKey should be inferred from the key.

It works correctly if I don't use the getter:

createConfigWithOption(() => ({
  key: ["1"],
  updateKey: (currentKey) => {
    const a = currentKey[0];
    //    ^? const a: string

    return [];
  },
}));

But it falls back to the default value of Key when I use the getter:

createConfigWithOption((get) => ({
  key: ["1"],
  updateKey: (currentKey) => {
    const a = currentKey[0];
    //    ^? const a: unknown
    
    return [];
  },
}));

Here is a Typescript playground if you want to try it out yourself.

I've been scratching my head for several hours to understand what's going on but didn't find an answer.

答案1

得分: 2

以下是您要翻译的部分:

查看microsoft/TypeScript#47599以获取有关此问题的权威描述。

TypeScript的类型推断算法远非完美;它实际上是一组启发式规则的集合,适用于广泛的用例,但不幸的是,有时会出现无法推断出显而易见的情况。一个可能出问题的地方是,编译器需要同时推断泛型类型参数以及上下文推断回调参数类型。

以下两者之间的区别:

createConfigWithOption(() => ({
  key: ["1"],
  updateKey: (currentKey) => {
    const a = currentKey[0];
    //    ^? const a: string

    return [];
  },
}));

createConfigWithOption((get) => ({
  key: ["1"],
  updateKey: (currentKey) => {
    const a = currentKey[0];
    //    ^? const a: unknown

    return [];
  },
}));

区别在于,在后者中,传递给createConfigWithOption的回调具有编译器要为您推断的回调参数get的类型。这使整个回调函数与上下文相关。一般来说,如果编译器不知道函数的参数类型,它也无法知道函数内部发生的事情的类型。当前的算法倾向于推迟上下文敏感的推断,直到推断出泛型类型参数。但这是个问题,因为让回调函数成为黑匣子意味着编译器不知道要为E泛型类型参数推断什么,因此它会退回到约束,之后get将被适当推断...但为时已晚,因为E比您想要的更宽泛。

请注意,在这个特定情况下,事实证明get在函数体内部实际上没有被使用,因此实际上没有任何依赖于其类型。但编译器对函数体内部的检查并不多。因此,像(get) =>这样使用会破坏推断。


这就是发生的情况。最近在这种循环上下文和泛型推断方面已经有一些改进,如microsoft/TypeScript#48538所述,但它们无法处理每种可能的情况,而您的代码是其中之一,仍然无法按您的期望工作。

解决方法要么完全将其省略,如您的第一个示例中的() =>,要么明确注释它的类型,以便编译器不必推断它:

createConfigWithOption((get: Getter) => ({
  key: ["1"],
  updateKey: (currentKey) => {
    const a = currentKey[0];
    //    ^? const a: string

    return [];
  },
}));

代码示例链接

英文:

See microsoft/TypeScript#47599 for an authoritative description of this issue.

TypeScript's type inference algorithm isn't perfect by any means; it's effectively a collection of heuristic rules that works well over a wide range of use cases, but there are unfortunately situations where it fails to infer something that a human developer would see as obvious. One place where things can go awry is if the compiler needs to infer generic type arguments at the same time it needs to contextually infer callback parameter types.

The difference between

createConfigWithOption(() => ({
  key: ["1"],
  updateKey: (currentKey) => {
    const a = currentKey[0];
    //    ^? const a: string

    return [];
  },
}));

and

createConfigWithOption((get) => ({
  key: ["1"],
  updateKey: (currentKey) => {
    const a = currentKey[0];
    //    ^? const a: unknown
    
    return [];
  },
}));

is that in the latter, the callback passed to createConfigWithOption has a callback parameter get whose type the compiler wants to infer for you. That makes the whole callback function context sensitive. Generally speaking, if the compiler doesn't know the parameter type of a function, it can't know the types of what's going on inside the function either. The current algorithm tends to delay context sensitive inferences until after generic type arguments are inferred. But this is a problem, since leaving the callback as a black box means that the compiler has no idea what to infer for the E generic type parameter, so it falls back to the constraint, after which get is inferred properly... but it's too late, since E is wider than you want.

Note that in this particular instance, it turns out that get is not used anywhere inside the function body, so in fact nothing actually depends on its type. But the compiler doesn't do much of an inspection into the function body to check for that sort of thing. So using (get)=> like that messes up inference for you.


So that's what's going on. There have been recent improvements in this sort of circular contextual-and-generic inference, as described in microsoft/TypeScript#48538, but they can't address every possible situation, and your code is one of those places that still doesn't work as you want.

The workarounds here are either to leave it out entirely like ()=> as in your first example, or to explicitly annotate it with its type so the compiler doesn't have to infer it:

createConfigWithOption((get: Getter) => ({
  key: ["1"],
  updateKey: (currentKey) => {
    const a = currentKey[0];
    //    ^? const a: string
    
    return [];
  },
}));

Playground link to code

huangapple
  • 本文由 发表于 2023年3月3日 23:55:33
  • 转载请务必保留本文链接:https://go.coder-hub.com/75629273.html
匿名

发表评论

匿名网友

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

确定