Typescript生成的辨识联合类型上的错误

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

Typescript error on generated discriminated unions

问题

我正在尝试编写一段代码,该代码将为类型的每个成员生成一个联合类型,并编写一个接受该类型对象和联合类型成员实例的函数。

以下是您目前的代码:

  1. interface RickAndMortyCharacter {
  2. species: string;
  3. origin: {
  4. name: string;
  5. url: string;
  6. };
  7. }
  8. type RickAndMortyCharacterChange = {
  9. [Prop in keyof RickAndMortyCharacter]: {
  10. updatedProps: Prop;
  11. newValue: RickAndMortyCharacter[Prop];
  12. };
  13. }[keyof RickAndMortyCharacter];
  14. function applyPropertyChange(
  15. state: RickAndMortyCharacter,
  16. payload: RickAndMortyCharacterChange
  17. ) {
  18. // 此行出现错误
  19. state[payload.updatedProps] = payload.newValue;
  20. return state;
  21. }

TypeScript 给出的错误是:

类型 'string | { name: string; url: string; }' 不能赋值给类型 'string & { name: string; url: string; }'。
类型 'string' 不能赋值给类型 'string & { name: string; url: string; }'。
类型 'string' 不能赋值给类型 '{ name: string; url: string; }'。

当我悬停在 RickAndMortyCharacterChange 类型上时,看到了以下类型声明:

  1. type RickAndMortyCharacterChange = {
  2. updatedProps: "species";
  3. newValue: string;
  4. } | {
  5. updatedProps: "origin";
  6. newValue: {
  7. name: string;
  8. url: string;
  9. };
  10. }

这似乎是正确的,因为 TypeScript 阻止我编写以下代码:

  1. const s: RickAndMortyCharacterChange = {
  2. updatedProps: "origin",
  3. newValue: "bonjour"
  4. };
  5. const s2: RickAndMortyCharacterChange = {
  6. updatedProps: "species",
  7. newValue: {
  8. name: "Hello",
  9. url: "Hoi"
  10. }
  11. }

我最近在一个 Angular 项目中使用了非常相似的方法,而且它完美地工作,我不明白为什么 TypeScript 在这里出现问题。

请注意,此问题可能是由于 TypeScript 版本或配置的差异引起的,或者是由于其他依赖项的影响。你可以尝试升级 TypeScript 版本或者检查项目中的其他配置,以确保一致性。

英文:

I'm trying to write a code that will generate a discriminated union for each member of a type and a function that will accept an object of that type and an instance of one of the union member.

here's the code I have so far

  1. interface RickAndMortyCharacter {
  2. species: string;
  3. origin: {
  4. name: string;
  5. url: string;
  6. };
  7. }
  8. type RickAndMortyCharacterChange = {
  9. [Prop in keyof RickAndMortyCharacter]: {
  10. updatedProps: Prop;
  11. newValue: RickAndMortyCharacter[Prop];
  12. };
  13. }[keyof RickAndMortyCharacter];
  14. function applyPropertyChange(
  15. state: RickAndMortyCharacter,
  16. payload: RickAndMortyCharacterChange
  17. ) {
  18. // Error on this line
  19. state[payload.updatedProps] = payload.newValue;
  20. return state;
  21. }

Also available here with the typescript playground

typescript gives me the following error
> Type 'string | { name: string; url: string; }' is not assignable to type 'string & { name: string; url: string; }'.
Type 'string' is not assignable to type 'string & { name: string; url: string; }'.
Type 'string' is not assignable to type '{ name: string; url: string; }'.(2322)

What is interesting is when i hover over the RickAndMortyCharacterChange type I see that the following type is declared:

  1. type RickAndMortyCharacterChange = {
  2. updatedProps: "species";
  3. newValue: string;
  4. } | {
  5. updatedProps: "origin";
  6. newValue: {
  7. name: string;
  8. url: string;
  9. };
  10. }

Which seems to be right to me since Typescript prevents me from writing this

  1. const s: RickAndMortyCharacterChange = {
  2. updatedProps: "origin",
  3. newValue: "bonjour"
  4. };
  5. const s2: RickAndMortyCharacterChange = {
  6. updatedProps: "species",
  7. newValue: {
  8. name: "Hello",
  9. url: "Hoi"
  10. }
  11. }

I have used a very very similar method in an Angular project recently and it worked perfectly I can not understand why typescript complains here

答案1

得分: 2

这实际上是 TypeScript 类型检查算法的一种限制,根本问题在于 microsoft/TypeScript#30581 中有所描述。当你有一个 联合类型 的表达式,并且编写代码其中这个表达式出现多次时,编译器会将每次出现的该表达式视为与原始表达式独立的。所以,如果有类似以下的情况:

  1. declare const state: RickAndMortyCharacter;
  2. declare const payload: {
  3. updatedProps: "species";
  4. newValue: string;
  5. } | {
  6. updatedProps: "origin";
  7. newValue: {
  8. name: string;
  9. url: string;
  10. };
  11. }
  12. const up = payload.updatedProps;
  13. // const up: "species" | "origin"
  14. const nv = payload.newValue;
  15. // const nv: string | { name: string; url: string; }

你会得到这个结果:

  1. const up = payload.updatedProps;
  2. // const up: "species" | "origin"
  3. const nv = payload.newValue;
  4. // const nv: string | { name: string; url: string; }

请注意,upnv 都是联合类型。但这失去了你关心的信息。如果你只知道 up 的类型是 "species" | "origin",以及 nv 的类型是 string | { name: string; url: string;},那么你不能再无错误地编写如下代码:

  1. state[up] = nv; // 出现错误!

因为如果 up"origin"nv 是一个 string 会怎样呢?你只知道这是不可能的,因为 upnv 都来自同一个 payload。但 TypeScript 不会分析 payload身份,它只关注类型。在 TypeScript 中没有所谓的“相关联的联合”。

因此,即使这是100%类型安全的赋值,它也会失败。他们无法通过让编译器多次分析 state[up] = nv 来解决这个问题,因为这可能会大幅破坏编译性能或使编译器变得不可预测。请参见 TS团队开发负责人的这个推文线程


幸运的是,有一种方法可以重构代码,以便编译器能够按照逻辑执行。这在 microsoft/TypeScript#47109 中有描述。这种方法是用 泛型 替换联合类型,泛型范围涵盖键样类型,然后在适当的 映射类型 中执行泛型 索引。这些索引-到-映射类型可以发生在单个 分布式对象类型 中(正如在该 GitHub 问题中所定义的)。

你的 RickAndMortyCharacterChange 本质上就是这样一种类型,但你可以将其更改为泛型(并仍然默认为完整联合):

  1. type RickAndMortyCharacterChange<K extends keyof RickAndMortyCharacter = keyof RickAndMortyCharacter> = {
  2. [P in K]: {
  3. updatedProps: P;
  4. newValue: RickAndMortyCharacter[P];
  5. };
  6. }[K];

现在,applyPropertyChange() 可以使用泛型 K,限制为 keyof RickAndMortyCharacter。你将传入类型为 RickAndMortyCharacterChange<K> 的值,然后一切都能正常工作:

  1. function applyPropertyChange<K extends keyof RickAndMortyCharacter>(
  2. state: RickAndMortyCharacter,
  3. payload: RickAndMortyCharacterChange<K>
  4. ) {
  5. state[payload.updatedProps] = payload.newValue; // 没问题
  6. return state;
  7. }

如果你将其拆分成 upnv,你将明白为什么:

  1. function applyPropertyChange<K extends keyof RickAndMortyCharacter>(
  2. state: RickAndMortyCharacter,
  3. payload: RickAndMortyCharacterChange<K>
  4. ) {
  5. const up = payload.updatedProps;
  6. // const up: K
  7. const nv = payload.newValue;
  8. // RickAndMortyCharacter[K]
  9. state[up] = nv; // 没问题
  10. return state;
  11. }

现在,up 的类型是 Knv 的类型是 RickAndMortyCharacter[K]。它们都是泛型。并且这个赋值被视为将右侧的 RickAndMortyCharacter[K] 赋给左侧的 RickAndMortyCharacter[K]。由于这些类型是相同的泛型,一切都按预期工作。


[代码的Playground链接](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgErAQawIIgCYCyA9lGAJ4DCAFnFIpFMgN4BQyyAzgA4QLAQcAXJzBRQAcwDcbZCWDjQw1u3Yg4AWwjCOoidJXIArlAA223SCkyAvtOssW5Hmgw58xUpRp0EDanEsIAB4AaWQIAA9IfA5kTAgyIhgXLFxCEnJ-HwYZAF44hKSUt3TPLPpoAD5kfOUVAG0ABWRQZBCAXSUZA0MuPDhIPEaoIi4hZEb9A2QQCAB3ADU4E0MtYrSPTO8KqCb2qfZbG3qO6UcyZ1Qa9

英文:

This is essentially a limitation of the TypeScript type checking algorithm, and the underlying issue is described in microsoft/TypeScript#30581. When you have an expression of a union type and write code where that expression appears multiple times, the compiler analyzes it by treating each utterance of that expression as if it were independent of the original. So given something like

  1. declare const state: RickAndMortyCharacter;
  2. declare const payload: {
  3. updatedProps: &quot;species&quot;;
  4. newValue: string;
  5. } | {
  6. updatedProps: &quot;origin&quot;;
  7. newValue: {
  8. name: string;
  9. url: string;
  10. };
  11. }

you get this:

  1. const up = payload.updatedProps;
  2. // const up: &quot;species&quot; | &quot;origin&quot;
  3. const nv = payload.newValue;
  4. // const nv: string | { name: string; url: string; }

Note that up and nv are both of union types. But that has lost information you care about. If all you know about up is that it's of type &quot;species&quot; | &quot;origin&quot;, and all you know about nv is that it's of type string | { name: string; url: string}, then you can't write this anymore without an error:

  1. state[up] = nv; // error!

Because what if up is &quot;origin&quot; but nv is a string? You only know that's impossible because both up and nv came from the same payload. But TypeScript doesn't analyze the identity of payload, it's looking at the type. There's no such thing as "correlated unions" in TypeScript.

So the assignment fails, even though it's 100% type safe. And they can't fix it by just making the compiler analyze state[up] = nv multiple times, without either massively destroying compiler performance or making the compiler unpredictable. See this twitter thread by the TS Team dev lead.


Luckily, there is a way to refactor the code so that the compiler does follow the logic. It's described at microsoft/TypeScript#47109. The approach is to replace unions with generics that range over keylike types, and then perform generic indexes into appropriate mapped types. These indexes-into-mapped types can happen inside a single distributive object type (as coined in that GitHub issue).

Your RickAndMortyCharacterChange is essentially such a type, but you can change it to make it generic (and still default to the full union):

  1. type RickAndMortyCharacterChange&lt;K extends keyof RickAndMortyCharacter
  2. = keyof RickAndMortyCharacter&gt; = {
  3. [P in K]: {
  4. updatedProps: P;
  5. newValue: RickAndMortyCharacter[P];
  6. };
  7. }[K];

And now applyPropertyChange() can be made generic in K constrained to keyof RickAndMortyCharacter. You will pass in a value of type RickAndMortyCharacterChange&lt;K&gt;, and suddenly everything works:

  1. function applyPropertyChange&lt;K extends keyof RickAndMortyCharacter&gt;(
  2. state: RickAndMortyCharacter,
  3. payload: RickAndMortyCharacterChange&lt;K&gt;
  4. ) {
  5. state[payload.updatedProps] = payload.newValue; // okay
  6. return state;
  7. }

If you break that into up and nv you'll see why:

  1. function applyPropertyChange&lt;K extends keyof RickAndMortyCharacter&gt;(
  2. state: RickAndMortyCharacter,
  3. payload: RickAndMortyCharacterChange&lt;K&gt;
  4. ) {
  5. const up = payload.updatedProps;
  6. // const up: K
  7. const nv = payload.newValue;
  8. // RickAndMortyCharacter[K]
  9. state[up] = nv; // okay
  10. return state;
  11. }

Now up is of type K and nv is of type RickAndMortyCharacter[K]. They are both generic. And the assignment is seen as assigning a RickAndMortyCharacter[K] on the right to a RickAndMortyCharacter[K] on the left. Since these types are identical generics, everything works as desired.


Playground link to code

答案2

得分: 1

以下是代码的翻译部分:

  1. 只是为了补充jcalz出色的回答,你可以创建一个实用类型,以便轻松将此模式应用于任何对象类型:
  2. type PropertyChange<T extends Object, K extends keyof T = keyof T> = {
  3. [P in K]: {
  4. updatedProps: P;
  5. newValue: T[P];
  6. };
  7. }[K];
  8. function applyPropertyChange<T extends Object, K extends keyof T = keyof T>(
  9. state: T,
  10. payload: PropertyChange<T, K>
  11. ) {
  12. state[payload.updatedProps] = payload.newValue;
  13. return state;
  14. }

用法:

  1. const character: RickAndMortyCharacter = {
  2. species: 'initial species',
  3. origin: { name: '', url: '' },
  4. };
  5. const change: PropertyChange<RickAndMortyCharacter> = {
  6. updatedProps: 'species',
  7. newValue: 'new species',
  8. }; // ok
  9. const change2: PropertyChange<RickAndMortyCharacter> = {
  10. updatedProps: 'origin',
  11. newValue: '',
  12. }; // error
  13. applyPropertyChange(character, change); // ok
  14. applyPropertyChange(
  15. character,
  16. { updatedProps: 'origin', newValue: '' }
  17. ); // error
  18. applyPropertyChange(
  19. character,
  20. { updatedProps: 'species', newValue: '' }
  21. ); // ok
  22. applyPropertyChange(
  23. { prop1: 'string' },
  24. { updatedProps: 'prop1', newValue: 'new string' }
  25. ); // ok
  26. applyPropertyChange(
  27. { prop1: 1 },
  28. { updatedProps: 'prop1', newValue: 'new string' }
  29. ); // error
  30. applyPropertyChange<{ prop1: number }>(
  31. { prop1: 'string' },
  32. { updatedProps: 'prop1', newValue: 'new string' }
  33. ); // error
英文:

Just to add on to jcalz awesome answer, you can make a utility type so you can easily apply this pattern to any object type:

  1. type PropertyChange&lt;T extends Object, K extends keyof T = keyof T&gt; = {
  2. [P in K]: {
  3. updatedProps: P;
  4. newValue: T[P];
  5. };
  6. }[K];
  7. function applyPropertyChange&lt;T extends Object, K extends keyof T = keyof T&gt;(
  8. state: T,
  9. payload: PropertyChange&lt;T, K&gt;
  10. ) {
  11. state[payload.updatedProps] = payload.newValue;
  12. return state;
  13. }

Usage

  1. const character: RickAndMortyCharacter = {
  2. species: &#39;initial species&#39;,
  3. origin: { name: &#39;&#39;, url: &#39;&#39; },
  4. };
  5. const change: PropertyChange&lt;RickAndMortyCharacter&gt; = {
  6. updatedProps: &#39;species&#39;,
  7. newValue: &#39;new species&#39;,
  8. }; // ok
  9. const change2: PropertyChange&lt;RickAndMortyCharacter&gt; = {
  10. updatedProps: &#39;origin&#39;,
  11. newValue: &#39;&#39;,
  12. }; // error
  13. applyPropertyChange(character, change); // ok
  14. applyPropertyChange(
  15. character,
  16. { updatedProps: &#39;origin&#39;, newValue: &#39;&#39; }
  17. ); // error
  18. applyPropertyChange(
  19. character,
  20. { updatedProps: &#39;species&#39;, newValue: &#39;&#39; }
  21. ); // ok
  22. applyPropertyChange(
  23. { prop1: &#39;string&#39; },
  24. { updatedProps: &#39;prop1&#39;, newValue: &#39;new string&#39; }
  25. ); // ok
  26. applyPropertyChange(
  27. { prop1: 1 },
  28. { updatedProps: &#39;prop1&#39;, newValue: &#39;new string&#39; }
  29. ); // error
  30. applyPropertyChange&lt;{ prop1: number }&gt;(
  31. { prop1: &#39;string&#39; },
  32. { updatedProps: &#39;prop1&#39;, newValue: &#39;new string&#39; }
  33. ); // error

huangapple
  • 本文由 发表于 2023年3月12日 08:09:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/75710331.html
匿名

发表评论

匿名网友

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

确定