TypeScript在减少对象键数组时推断’never’类型的累加器对象。

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

TypeScript infer the 'never' type in accumulator object when reducing an Array of object keys

问题

const getObjectKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;

interface AB {
    a: string;
    b: number;
}
export function test(config: AB) {
    Utils.getObjectKeys(config).reduce((memo, key) => {
        memo[key] = config[key];
        return memo;
    }, {} as AB);
}
memo[key] - TS2322: Type 'string | number' is not assignable to type 'never'. Type 'string' is not assignable to type 'never'.

It's only working if I rewrite it like (memo as any)[key]. Why can't I use it like this in TypeScript? How to initialize the accumulator memo to fix this problem?
英文:
const getObjectKeys = Object.keys as &lt;T extends object&gt;(obj: T) =&gt; Array&lt;keyof T&gt;;

interface AB {
    a: string;
    b: number;
}
export function test(config: AB) {
    Utils.getObjectKeys(config).reduce((memo, key) =&gt; {
        **memo[key]** = config[key];
        return memo;
    }, {} as AB);
}

memo[key] - TS2322: Type 'string | number' is not assignable to type 'never'.   Type 'string' is not assignable to type 'never'.

It's only working if i rewrite it like (memo as any)[key]. Why i can't use it like this in typescript? How to initialize accumulator memo to fix this problem?

答案1

得分: 0

编译器不幸不能遵循单行代码memo[key] = config[key]的逻辑,当key联合类型(例如keyof AB)时。它不会分析该行代码,关注变量的标识,而只看类型。但我们之所以知道memo[key] = config[key]是安全的,是因为相同的key在两边都使用了。如果我们有key1key2,它们都是keyof AB类型,那么编译器的分析将更为合理。

这种分析实际上在microsoft/TypeScript#30769中实现了。对于memo[key1] = config[key2],编译器知道从config[key2]读取,其中key2是一个联合类型,将导致另一个联合类型string | number。但是将值分配给memo[key1]可能是不安全的。毕竟,key1key2可能是联合类型的不同成员。这种赋值安全的唯一方式是,无论key1是什么,都可以将config[key2]分配给memo[key1]。这意味着config[key2]必须同时是stringnumber:即交叉类型string & number。但是没有这种类型的值;这是不可能的。因此,编译器将该交叉类型折叠为不可能的never类型。因此,编译器将正确地抱怨memo[key1] = config[key2]是一个错误,因为在不知道key1是什么的情况下,你无法安全地将任何内容分配给它。这种分析通常很有用,可以捕获真正的错误。

不幸的是,当你使用key而不是key1key2时,编译器会过于谨慎,因为它担心你可能会将string分配给number,或者反之。

那么我们该怎么办呢?从microsoft/TypeScript#30769的评论中可以看出,使TypeScript遵循预期逻辑的方法是将key的类型设置为泛型generics)类型K,而不是联合类型本身。也就是说,你可以将reduce()回调函数定义为泛型,如下所示:

function test(config: AB) {
    getObjectKeys(config).reduce(<K extends keyof AB>(memo: AB, key: K) => {
        memo[key] = config[key];
        return memo;
    }, {} as AB);
}

这样可以工作,因为赋值的两边都被视为泛型类型AB[K]。事实证明,这实际上与联合类型的情况一样不安全(你可以将key1key2设置为类型K,并将K设置为完整的联合类型keyof AB,这也是允许的)。但正如评论中所说,TypeScript允许某些类型的不安全行为,包括相同的泛型索引访问类型之间的赋值。

代码演示链接

英文:

The compiler unfortunately can't follow the logic of the single line memo[key] = config[key] when key is of a union type like keyof AB. It doesn't analyze that line by paying attention to the identities of the variables; it just looks at the types. But the only reason we know memo[key] = config[key] is okay is because the identical key is used on both sides. If we had key1 and key2, both of type keyof AB, then the compiler's analysis would be more reasonable.

That analysis is implemented in microsoft/TypeScript#30769. Given memo[key1] = config[key2], the compiler knows that reading from config[key2] where key2 is a union results in another union, string | number. But assigning to memo[key1] is possibly unsafe. After all, key1 and key2 might be different members of the union. The only way for such an assignment to be safe is if config[key2] would be assignable to memo[key1] no matter what key1 was. That would mean config[key2] would have to be both a string and a number: that is, the intersection string &amp; number. But there is no value of that type; it's impossible. And so the compiler collapses that intersection to the impossible never type. So the compiler would rightly complain that memo[key1] = config[key2] is an error, since, without knowing what key1 is, you can't safely assign anything to it. Such analysis is generally useful and catches real errors.

Unfortunately, when you have key instead of key1 and key2, then the compiler is overly cautious, since it's worried about the impossible event that you're assigning a string to a number or vice versa.


So what can we do? From a comment from the implementer of ms/TS#30769, it is apparent that the way to make TypeScript follow the intended logic is to make key's type a generic type K constrained to a union instead of a union itself. That is, you make the reduce() callback generic as follows:

function test(config: AB) {
    getObjectKeys(config).reduce(&lt;K extends keyof AB&gt;(memo: AB, key: K) =&gt; {
        memo[key] = config[key];
        return memo;
    }, {} as AB);
}

This works because both sides of the assignment are seen as the generic type AB[K]. It turns out that this is actually unsafe in the same way as with a union one would be (you could make key1 and key2 of type K, and make K the full union keyof AB, and it would be allowed). But, as said in that comment, TypeScript admits certain types of unsafe behavior, and assignments between identical generic indexed access types are allowed.

Playground link to code

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

发表评论

匿名网友

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

确定