属性 ‘value’ 的类型不兼容 / ts(2322)

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

Types of property 'value' are incompatible / ts(2322)

问题

在简短的描述中,我有一个类型 FieldHeader,用于创建 Field,通过评估 get 方法与一个 Item 相关:

interface Item {
  id: string;
}

interface BaseFieldHeader<
  item extends Item,
  value extends boolean | number | string
> {
  get: (item: item) => value;
}

type FieldHeader<item extends Item> =
  | (BaseFieldHeader<item, boolean> & {
    type: 'boolean';
  })
  | (BaseFieldHeader<item, number> & {
    type: 'number';
  })
  | (BaseFieldHeader<item, string> & {
    type: 'string';
  });

type 将始终匹配 get 方法的返回类型或 value 的类型。

因此,设置如下:

type TableHeader<item extends Item> = Record<
  string,
  FieldHeader<item>
>;

并且,假设有一个 Item User,可以像这样从 TableHeader 创建字段:

interface User extends Item {
  age: number;
}

const tableHeader: TableHeader<User> = {
  age: {
    get: (user) => user.age,
    type: "number"
  }
}

const user: User = {
  id: "frank",
  age: 34
}

const tableRow: Field[] = Object.entries(tableHeader).map(
  ([key, field]) =>
  ({
    key,
    value: field.get(user),
    type: field.type,
  }
  )
)

为什么会出现错误:

Type '{ key: string; value: string | number | boolean; type: "string" | "number" | "boolean"; }[]' is not assignable to type 'Field[]'.
  Type '{ key: string; value: string | number | boolean; type: "string" | "number" | "boolean"; }' is not assignable to type 'Field'.
    Type '{ key: string; value: string | number | boolean; type: "string" | "number" | "boolean"; }' is not assignable to type 'BaseField<string> & { type: "string"; }'.
      Type '{ key: string; value: string | number | boolean; type: "string" | "number" | "boolean"; }' is not assignable to type 'BaseField<string>'.
        Types of property 'value' are incompatible.
          Type 'string | number | boolean' is not assignable to type 'string'.
            Type 'number' is not assignable to type 'string'.(2322)

Playground

英文:

In short I have a type FieldHeader

interface Item {
  id: string;
}

interface BaseFieldHeader<
  item extends Item,
  value extends boolean | number | string
> {
  get: (item: item) => value;
}

type FieldHeader<item extends Item> =
  | (BaseFieldHeader<item, boolean> & {
    type: 'boolean';
  })
  | (BaseFieldHeader<item, number> & {
    type: 'number';
  })
  | (BaseFieldHeader<item, string> & {
    type: 'string';
  });

that is used to create Fields by evaluating get with an Item:

interface BaseField<value extends boolean | number | string> {
  key: string;
  value: value;
}

type Field =
  | (BaseField<boolean> & {
    type: 'boolean';
  })
  | (BaseField<number> & {
    type: 'number';
  })
  | (BaseField<string> & {
    type: 'string';
  });

type will always match the return type of get / the type of value resp.

Thus, setting

type TableHeader<item extends Item> = Record<
  string,
  FieldHeader<item>
>;

and—given an Item User—creating fields from a TableHeader like so

interface User extends Item {
  age: number;
}

const tableHeader: TableHeader<User> = {
  age: {
    get: (user) => user.age,
    type: "number"
  }
}

const user: User = {
  id: "frank",
  age: 34
}

const tableRow: Field[] = Object.entries(tableHeader).map(
  ([key, field]) =>
  ({
    key,
    value: field.get(user),
    type: field.type,
  }
  )
)

why do I get the error

Type '{ key: string; value: string | number | boolean; type: "string" | "number" | "boolean"; }[]' is not assignable to type 'Field[]'.
  Type '{ key: string; value: string | number | boolean; type: "string" | "number" | "boolean"; }' is not assignable to type 'Field'.
    Type '{ key: string; value: string | number | boolean; type: "string" | "number" | "boolean"; }' is not assignable to type 'BaseField<string> & { type: "string"; }'.
      Type '{ key: string; value: string | number | boolean; type: "string" | "number" | "boolean"; }' is not assignable to type 'BaseField<string>'.
        Types of property 'value' are incompatible.
          Type 'string | number | boolean' is not assignable to type 'string'.
            Type 'number' is not assignable to type 'string'.(2322)

? Playground

答案1

得分: 1

以下是您要翻译的内容:

TypeScript目前在您的代码中存在问题,问题在于它无法多次分析单个值{ key, value: field.get(user), type: field.type }的类型。它一次性完成所有分析,因此value联合类型string | number | boolean,而type是联合类型"string" | "number" | "boolean",它们之间的任何关联性都已丢失。因此,TypeScript无法直接支持“关联联合类型”,如microsoft/TypeScript#30581中所描述的那样。

如果编译器能够分析每个field类型的可能联合成员的值,那么它将看到每个成员都单独工作,从而整体工作。但是,如果编译器在遇到联合类型的值时都这样做,对性能将造成灾难性的破坏。因此,这是不可能的。您可以通过使用switch语句或一系列if/else子句手动引导编译器进行分析,但然后您将多次编写相同的值。

简单的方法就是使用类型断言,因为您比编译器更聪明:

const tableRow: Field[] = Object.entries(tableHeader).map(
  ([key, field]) => ({
    key,
    value: field.get(user),
    type: field.type,
  } as Field)
);

但接下来就是您要确保自己做得正确,而不是编译器。您已经接管了编译器的类型安全验证。


有一种推荐的方法可以处理编译器可以跟踪的关联联合类型。这在microsoft/TypeScript#47109中有描述,涉及到重构以使用泛型而不是联合类型。泛型是唯一真正支持让编译器分析“多个情况”的方法。

特别是,您希望根据一些基本的键值接口和mapped types以及泛型indexes into这些类型来表示操作。

对于您的代码示例,可能如下所示。基本的键值接口是:

interface TypeMap {
  boolean: boolean;
  number: number;
  string: string;
}

它表示type属性与相应数据类型之间的关系。然后,我们可以按如下方式重写FieldHeaderTableFieldHeader类型:

type FieldHeader<I extends Item, K extends keyof TypeMap = keyof TypeMap> =
  { [P in K]: {
    type: P,
    get: (item: I) => TypeMap[P]
  } }[K]

type Field<K extends keyof TypeMap = keyof TypeMap> =
  { [P in K]: {
    type: P,
    key: string;
    value: TypeMap[P]
  } }[K]

这两者都是分布式对象类型,通过立即索引到映射类型,它们立即评估为联合。它们也是为了泛型而编写的,因此,例如,Field<"string">只是联合的string相关成员。类似地,TableHeader扩展为泛型:

type TableHeader<I extends Item, K extends keyof TypeMap = keyof TypeMap> =
  Record<string, FieldHeader<I, K>>;

现在我们可以编写代码,将项目/表头对转换为字段数组。为了使编译器理解这一点,它需要在K extends keyof TypeMap中是泛型的,因此我们必须制作一个泛型函数。这可以是匿名函数表达式,但给它一个名称可能更清晰:

function mapTableHeaderToFieldArray<I extends Item, K extends keyof TypeMap>(
  item: I, tableHeader: TableHeader<I, K>): Field<K>[] {
  return Object.entries(tableHeader).map(([key, field]) => ({
    key,
    type: field.type,
    value: field.get(item)
  }))
}

这会编译而不会出错。现在我们终于可以使用它:

const user: User = {
  id: "frank",
  age: 34
}

const tableRow: Field[] =
  mapTableHeaderToFieldArray(user, tableHeader);

太好了,它运行正常!


在上述重构中,现在所有内容都可以编译而不出错,这是我们可以让编译器为我们验证类型安全性的最接近的方式。但是否值得取决于您的用例。如果您真的希望编译器在出错时提出投诉,并且不想编写大量手动案例,那么这可能值得一试。但如果您更看重惯用代码和开发人员的生产力,而不是编译器验证的类型安全性,那么您可能只需使用类型断言并继续前进;这样做并不是错误的,只要您意识到这些选项并评估了它们相对于您的需求的适用性。

英文:

The problem TypeScript is having with your code is that it cannot analyze the type of single value { key, value: field.get(user), type: field.type } multiple times. It does so all at once, and therefore value is of the union type string | number | boolean and type is of the union type &quot;string&quot; | &quot;number&quot; | &quot;boolean&quot;, and any correlation between them has been lost. TypeScript therefore cannot directly support "correlated union types", as described in microsoft/TypeScript#30581.

If the compiler could analyze that value once for each possible union member of the type of field, then it would see that each member works separately and thus works overall. But if the compiler did this whenever it encountered a value of a union type, it would be catastrophically damaging to performance. So that's not possible. You could manually lead the compiler through analysis by using a switch statement or series of if/else clauses, but then you'd be writing the same value multiple times.

The easiest approach is just to use a type assertion because you're smarter than the compiler:

const tableRow: Field[] = Object.entries(tableHeader).map(
  ([key, field]) =&gt; ({
    key,
    value: field.get(user),
    type: field.type,
  } as Field)
);

but then it's your job to make sure you did it right, not the compiler's. You've taken over type safety verification from the compiler.


There is a recommended approach to dealing with correlated unions in a way that the compiler can follow. It's described in microsoft/TypeScript#47109 and involves refactoring to use generics instead of unions. Generics are the only really supported way of getting the compiler to analyze "multiple cases at once".

In particular you want to represent the operations in terms of some basic key-value interface and mapped types over that interface, as well as generic indexes into such types.

For your code example it might look like this. The basic key-value interface is:

interface TypeMap {
  boolean: boolean;
  number: number;
  string: string;
}

which represents the relationship between the type property and the corresponding data type. Then we can rewrite your FieldHeader and TableFieldHeader types in terms of it as follows:

type FieldHeader&lt;I extends Item, K extends keyof TypeMap = keyof TypeMap&gt; =
  { [P in K]: {
    type: P,
    get: (item: I) =&gt; TypeMap[P]
  } }[K]

type Field&lt;K extends keyof TypeMap = keyof TypeMap&gt; =
  { [P in K]: {
    type: P,
    key: string;
    value: TypeMap[P]
  } }[K]

These are both distributive object types that evaluate to unions by immediately indexing into mapped types. They are also written to be generic so that, say, Field&lt;&quot;string&quot;&gt; is just the string-related member of the union. Similarly TableHeader is expanded to be generic that way:

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

And now we can write the code to convert an item/table-header pair into an array of fields. This needs to be generic in K extends keyof TypeMap in order for the compiler to understand that this works, so we have to make a generic function. This could be an anonymous function expression but it's probably clearer to give it a name:

function mapTableHeaderToFieldArray&lt;I extends Item, K extends keyof TypeMap&gt;(
  item: I, tableHeader: TableHeader&lt;I, K&gt;): Field&lt;K&gt;[] {
  return Object.entries(tableHeader).map(([key, field]) =&gt; ({
    key,
    type: field.type,
    value: field.get(item)
  }))
}

That compiles without error. And now we can finally use it:

const user: User = {
  id: &quot;frank&quot;,
  age: 34
}

const tableRow: Field[] =
  mapTableHeaderToFieldArray(user, tableHeader);

Hooray, it works!


In the above refactoring everything now compiles without error, and it is as close as we can get to having the compiler verify type safety for us. But is it worth it? That depends strongly on your use cases. If you really want the compiler to complain if you make a mistake and you don't want to write out a bunch of manual cases, then it might be worth it. But if you value idiomatic code and developer productivity more than you value compiler-verified type safety, then you should probably just use a type assertion and move on; it's not wrong to do so, as long as you're aware of the options and have assessed their relative applicability to your needs.

Playground link to code

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

发表评论

匿名网友

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

确定