Argument of type ‘string | number | boolean’ is not assignable to parameter of type ‘never’. ts2345

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

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 属性的类型没有被正确推断出来。根据给定的 ItemTableHeader 定义,我可以定义一个 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&#39;m using the following union type

```lang-ts
interface TypeMap {
  boolean: boolean;
  number: number;
  string: string;
}

interface Item {
  id: string;
}

type FieldHeader&lt;I extends Item&gt; = {
  [Type in keyof TypeMap]: {
    compare: (a: TypeMap[Type], b: TypeMap[Type]) =&gt; number;
    filter: (value: TypeMap[Type], input: TypeMap[Type]) =&gt; boolean;
    get: (item: I) =&gt; TypeMap[Type] | null | undefined;
    type: Type;
  };
}[keyof TypeMap];

Setting

type TableHeader&lt;I extends Item&gt; = Record&lt;
  string,
  FieldHeader&lt;I&gt;
&gt;;

type Filter&lt;I extends Item, T extends TableHeader&lt;I&gt;&gt; = {
  [Field in keyof T]: TypeMap[T[Field][&#39;type&#39;]];      // this isn&#39;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&lt;I extends Item, T extends TableHeader&lt;I&gt;&gt;(item: I, tableHeader: T, filter: Filter&lt;I, T&gt;) {
  Object.entries(filter).forEach(([key, input]) =&gt; {
    const value = tableHeader[key].get(item);

    if (value != null) tableHeader[key].filter(value, input);
  })
}

I'm getting the error

Argument of type &#39;string | number | boolean&#39; is not assignable to parameter of type &#39;never&#39;.
  Type &#39;string&#39; is not assignable to type &#39;never&#39;.(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(),因为它不允许我们表示 KFilter<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&lt;I&gt; to default to a union, but we want to be able to pick particular K:

type FieldHeader&lt;I extends Item, K extends keyof TypeMap = keyof TypeMap&gt; = {
  [P in K]: {
    compare: (a: TypeMap[P], b: TypeMap[P]) =&gt; number;
    filter: (value: TypeMap[P], input: TypeMap[P]) =&gt; boolean;
    get: (item: I) =&gt; TypeMap[P] | null | undefined;
    type: P;
  };
}[K];

And now your types need to be generic for that mapping T:

type TableHeader&lt;I extends Item, T extends Record&lt;keyof T, keyof TypeMap&gt;&gt; = {
  [K in keyof T]: FieldHeader&lt;I, T[K]&gt;
}

type Filter&lt;I extends Item, T extends Record&lt;keyof T, keyof TypeMap&gt;&gt; = {
  [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&lt;MyItem, { selected: &quot;boolean&quot; }&gt; = {
  selected: {
    compare: (a, b) =&gt; +a - +b,
    filter: (value, input) =&gt; value === input,
    get: (video) =&gt; video.selected,
    type: &#39;boolean&#39;,
  },
};

const filter: Filter&lt;MyItem, { selected: &quot;boolean&quot; }&gt; = {
  // ts should throw an error here, as type of &quot;selected&quot; should be &#39;boolean&#39;
  selected: &quot;&quot;,

  // also only props of &quot;tableHeader&quot; should be allowed
  foo: 123
}

And finally your applyFilter() function is also generic in T like this:

function applyFilter&lt;I extends Item, T extends Record&lt;keyof T, keyof TypeMap&gt;&gt;(
  item: I, tableHeader: TableHeader&lt;I, T&gt;, filter: Filter&lt;I, T&gt;
) {
  (Object.keys(filter) as Array&lt;keyof T&gt;).forEach(&lt;K extends keyof T&gt;(key: K) =&gt; {
    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&lt;I, T&gt;[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]]) =&gt; boolean, so everything type checks.

Playground link to code

huangapple
  • 本文由 发表于 2023年5月28日 15:06:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/76350339.html
匿名

发表评论

匿名网友

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

确定