英文:
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 <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 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
在两边都使用了。如果我们有key1
和key2
,它们都是keyof AB
类型,那么编译器的分析将更为合理。
这种分析实际上在microsoft/TypeScript#30769中实现了。对于memo[key1] = config[key2]
,编译器知道从config[key2]
读取,其中key2
是一个联合类型,将导致另一个联合类型string | number
。但是将值分配给memo[key1]
可能是不安全的。毕竟,key1
和key2
可能是联合类型的不同成员。这种赋值安全的唯一方式是,无论key1
是什么,都可以将config[key2]
分配给memo[key1]
。这意味着config[key2]
必须同时是string
和number
:即交叉类型string & number
。但是没有这种类型的值;这是不可能的。因此,编译器将该交叉类型折叠为不可能的never
类型。因此,编译器将正确地抱怨memo[key1] = config[key2]
是一个错误,因为在不知道key1
是什么的情况下,你无法安全地将任何内容分配给它。这种分析通常很有用,可以捕获真正的错误。
不幸的是,当你使用key
而不是key1
和key2
时,编译器会过于谨慎,因为它担心你可能会将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]
。事实证明,这实际上与联合类型的情况一样不安全(你可以将key1
和key2
设置为类型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 & 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(<K extends keyof AB>(memo: AB, key: K) => {
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论