英文:
Argument of type 'string | number | boolean' is not assignable to parameter of type 'never'. ts2345
问题
我正在使用以下联合类型
```lang-ts
interface TypeMap {
boolean: boolean;
number: number;
string: string;
}
interface Item {
id: string;
}
type FieldHeader<I extends Item> = {
[Type in keyof TypeMap]: {
compare: (a: TypeMap[Type], b: TypeMap[Type]) => number;
filter: (value: TypeMap[Type], input: TypeMap[Type]) => boolean;
get: (item: I) => TypeMap[Type] | null | undefined;
type: Type;
};
}[keyof TypeMap];
设置
type TableHeader<I extends Item> = Record<
string,
FieldHeader<I>
>;
type Filter<I extends Item, T extends TableHeader<I>> = {
[Field in keyof T]: TypeMap[T[Field]['type']]; // 这部分不起作用
}
首先,Filter
属性的类型没有被正确推断出来。根据给定的 Item
和 TableHeader
定义,我可以定义一个 Filter
,其中包含任何属性键和任何值,而不会收到任何错误,参见这里(第44行)。
如果我设置
function applyFilter<I extends Item, T extends TableHeader<I>>(item: I, tableHeader: T, filter: Filter<I, T>) {
Object.entries(filter).forEach(([key, input]) => {
const value = tableHeader[key].get(item);
if (value != null) tableHeader[key].filter(value, input);
})
}
我会收到错误消息
类型“string | number | boolean”的参数不能赋给类型“never”的参数。
类型“string”不能赋给类型“never”。(2345)
(参见这里,第56行)这并不完全令人惊讶,问题是,我如何解决这个错误?
1: https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgCoE8AOECydPIDeAUMsgEYD2lANhHCAFwXV0MDcpyIArgLblozXgOicyAZzBRQAc2ZSZIWZwC xYqEixEKAJKQ%20RLsAAmC6XLUawWFADFgEGqYAS9U9AA8e5BAAekCCmEsgGEHwAfMgAvMZkANoY2MigyADWEOiUMGh2eJgAuswkZGQIlHyYcFAQzAAUcMzJuPhJdoUANBTN%20W0thQCUsdEiglDiZTDANNoNAG5wNDx1edgF7dhdqSCYPGC96-0dwzHRVLT0IJNkshAHyPXAhsx6p9EtGwPIAD7cPDQaL9kDxghBpiAIKYbshbNhDhBJqo1AlMtlcp98IVOMQ4ShUHByHR3HBPFAfH5AhBgqFwlFYsgAEoQCpQUxeLiKOSdLiOZxuDzePSRYiRHF45COWZCylBEJhQzdVCy6nyglEiAksk%20SLROKlZAJPkuHYZLI5NDFNatTBJI1OFyFBIAcjxzsK2OI6g0mnA0HgSGQOHQdJVNIVEXiyAkzhZkHMLEuHC9GgqICksMJxMFUGaWc1Oa8wbpeqjMboCHjJS45Uq1VqDTg3XI72QAGo4MgALTt8g8srIabS3OPRbLCDdUB7MCtscrWIxOJT-b9sp3B71eZmCCUWfbygAOnLcahq7IeOYzoubBAztXqh5yNTlHTYEHMzmko-3mLithdgtMB8y1aBSwNAB6cDYVCCQAAtKABUxYVgqBKAAd2QBg-CgVCoGQWDoAnTDQglC0ACJj0rKEyOjeDEIoFAr1YK5nU5WMqITAAGHkuEgzCaAkShkBfGh0GQTBUMwUJyKAjUQKgGi4IQk1BH4mh0KhLgYGoZgAEYACYAGYU2IGBQUrYAX0wzBMFEqVtApAI5VpP9lSc1VQnVbNSSFXUnheMJulk7yyWaboh0-eyhSVSJhgNAB5cgACs4wPalLAgCR6gi6BBgPbSoAAUUQWD6nqVEsknXZ9iGEYo1rV9kDnFA4mCgsfKgCr0EKA9138iJBhxAdgFyTclnnABCOJeEBYY2vkrqepyqAxvHKrp0GrhVEGL0gA
<details>
<summary>英文:</summary>
I'm using the following union type
```lang-ts
interface TypeMap {
boolean: boolean;
number: number;
string: string;
}
interface Item {
id: string;
}
type FieldHeader<I extends Item> = {
[Type in keyof TypeMap]: {
compare: (a: TypeMap[Type], b: TypeMap[Type]) => number;
filter: (value: TypeMap[Type], input: TypeMap[Type]) => boolean;
get: (item: I) => TypeMap[Type] | null | undefined;
type: Type;
};
}[keyof TypeMap];
Setting
type TableHeader<I extends Item> = Record<
string,
FieldHeader<I>
>;
type Filter<I extends Item, T extends TableHeader<I>> = {
[Field in keyof T]: TypeMap[T[Field]['type']]; // this isn't working
}
firstly, the type of properties of Filter
is not deduced correctly. Given an Item
and TableHeader
definition I could define a Filter
with any property key and any value without getting any errors, see here (l. 44)
And if I set
function applyFilter<I extends Item, T extends TableHeader<I>>(item: I, tableHeader: T, filter: Filter<I, T>) {
Object.entries(filter).forEach(([key, input]) => {
const value = tableHeader[key].get(item);
if (value != null) tableHeader[key].filter(value, input);
})
}
I'm getting the error
Argument of type 'string | number | boolean' is not assignable to parameter of type 'never'.
Type 'string' is not assignable to type 'never'.(2345)
(see here, l. 56) which is not totally surprising, question is, how can I resolve this error?
答案1
得分: 1
TypeScript 无法跟踪 联合类型 之间的相关性;如果您有一个类似于 forEach()
回调主体的单个代码块,编译器只能分析它一次。如果在这样的代码块中有多个表达式依赖于相同的联合类型值,编译器通常会将这些表达式视为联合类型本身,但会独立处理它们。因此,依赖于联合键的函数将变成函数的联合。依赖于联合键的参数将变成参数的联合。但是,您不能安全地使用联合参数来调用联合函数;编译器将坚持要求参数的交集。这是 microsoft/TypeScript#30581 的主题。
在这里最简单的方法是大量使用 类型断言 来消除错误,因为您确信您做得对。
但是,如果您希望编译器检查您的操作,您可以进行相当大规模的重构,以使用 泛型、索引 进入 映射类型,如 microsoft/TypeScript#47109 中所述。
这段代码已经经过一轮这样的重构,但现在您需要对映射类型 T
从表头键到特定字段类型进行重构。我们必须在 K extends keyof TypeMap
中使 FieldHeader
成为泛型;FieldHeader<I>
默认为联合,但我们希望能够选择特定的 K
:
type FieldHeader<I extends Item, K extends keyof TypeMap = keyof TypeMap> = {
[P in K]: {
compare: (a: TypeMap[P], b: TypeMap[P]) => number;
filter: (value: TypeMap[P], input: TypeMap[P]) => boolean;
get: (item: I) => TypeMap[P] | null | undefined;
type: P;
};
}[K];
现在,您的类型需要在该映射 T
中进行泛型化:
type TableHeader<I extends Item, T extends Record<keyof T, keyof TypeMap>> = {
[K in keyof T]: FieldHeader<I, T[K]>;
}
type Filter<I extends Item, T extends Record<keyof T, keyof TypeMap>> = {
[K in keyof T]: TypeMap[T[K]>;
}
因此,您需要在声明这些类型的变量时指定这一点(您可以使用实用函数来推断,而不是手动指定,但我不会在这方面深入讨论):
const tableHeader: TableHeader<MyItem, { selected: "boolean" }> = {
selected: {
compare: (a, b) => +a - +b,
filter: (value, input) => value === input,
get: (video) => video.selected,
type: 'boolean',
},
};
const filter: Filter<MyItem, { selected: "boolean" }> = {
// ts should throw an error here, as type of "selected" should be 'boolean'
selected: "",
// also only props of "tableHeader" should be allowed
foo: 123
}
最后,您的 applyFilter()
函数也是在 T
中进行泛型化的:
function applyFilter<I extends Item, T extends Record<keyof T, keyof TypeMap>>(
item: I, tableHeader: TableHeader<I, T>, filter: Filter<I, T>
) {
(Object.keys(filter) as Array<keyof T>).forEach(<K extends keyof T>(key: K) => {
const input = filter[key];
const value = tableHeader[key].get(item);
if (value != null) tableHeader[key].filter(value, input);
});
}
请注意,我们仍然需要对编译器进行类型断言,以便它能够看到 filter
的条目与 keyof T
有关,事实上,我们不能使用 Object.entries()
,因为它不允许我们表示 K
和 Filter<I, T>[K]
之间所需的关系。但除此之外,一切都有效;编译器可以看到 input
是类型 TypeMap[T[K]]
,value
是类型 TypeMap[T[K]]
(经过检查以确保不为空值),并且 tableHeader[key].filter
的类型是 (value: TypeMap[T[K]], input: TypeMap[T[K]]) => boolean
,因此一切都能通过类型检查。
英文:
TypeScript cannot track correlations across union types; if you've got a single code block like your forEach()
callback body, the compiler can only analyze it once. If you have multiple expressions in such a code block that depend on the same union-typed value, the compiler will usually just treat these expressions as being of union types themselves, but treat them independently. So a function that depends on a union key will become a union of functions. And an argument that depends on a union key will become a union of arguments. But you can't call a union of functions with a union of arguments safely; the compiler will insist on an intersection of arguments. This is the subject of microsoft/TypeScript#30581.
The easiest approach here is to just use type assertions liberally to quiet the errors because you're sure you're doing it right.
If you want the compiler to type check what you're doing, though, you can do a fairly large refactoring to use generic indexes into mapped types as described in microsoft/TypeScript#47109.
This code has already gone through one round of such refactoring, but now you need to do it for the mapping type T
from table header keys to the particular field type. We have to keep FieldHeader
generic in K extends keyof TypeMap
to do it; it's fine for FieldHeader<I>
to default to a union, but we want to be able to pick particular K
:
type FieldHeader<I extends Item, K extends keyof TypeMap = keyof TypeMap> = {
[P in K]: {
compare: (a: TypeMap[P], b: TypeMap[P]) => number;
filter: (value: TypeMap[P], input: TypeMap[P]) => boolean;
get: (item: I) => TypeMap[P] | null | undefined;
type: P;
};
}[K];
And now your types need to be generic for that mapping T
:
type TableHeader<I extends Item, T extends Record<keyof T, keyof TypeMap>> = {
[K in keyof T]: FieldHeader<I, T[K]>
}
type Filter<I extends Item, T extends Record<keyof T, keyof TypeMap>> = {
[K in keyof T]: TypeMap[T[K]];
}
And so you'll need to specify that when declaring variables of these types (you can use utility functions to infer instead of manually specifying, but I won't digress on that):
const tableHeader: TableHeader<MyItem, { selected: "boolean" }> = {
selected: {
compare: (a, b) => +a - +b,
filter: (value, input) => value === input,
get: (video) => video.selected,
type: 'boolean',
},
};
const filter: Filter<MyItem, { selected: "boolean" }> = {
// ts should throw an error here, as type of "selected" should be 'boolean'
selected: "",
// also only props of "tableHeader" should be allowed
foo: 123
}
And finally your applyFilter()
function is also generic in T
like this:
function applyFilter<I extends Item, T extends Record<keyof T, keyof TypeMap>>(
item: I, tableHeader: TableHeader<I, T>, filter: Filter<I, T>
) {
(Object.keys(filter) as Array<keyof T>).forEach(<K extends keyof T>(key: K) => {
const input = filter[key];
const value = tableHeader[key].get(item);
if (value != null) tableHeader[key].filter(value, input);
});
}
Note that we still need a type assertion for the compiler to see that filter
's entries has anything to do with keyof T
, and in fact we can't use Object.entries()
because that won't let us represent the relationship between K
and Filter<I, T>[K]
needed. But otherwise it works; the compiler can see that input
is of type TypeMap[T[K]]
, and that value
is of type TypeMap[T[K]]
(after being checked for nullishness) and that tableHeader[key].filter
is of type (value: TypeMap[T[K]], input: TypeMap[T[K]]) => boolean
, so everything type checks.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论