TypeScript 具有调用签名和索引签名的类型的不一致类型兼容性,

ix0qys7i  于 9个月前  发布在  TypeScript
关注(0)|答案(8)|浏览(143)

TypeScript 版本: 2.9.0-dev
搜索词: index signature intersection
代码

  1. type INOk = {
  2. (): string;
  3. [name: string] : number;
  4. }
  5. type IOk = {
  6. (): string;
  7. } & {
  8. [name: string] : number;
  9. }
  10. declare let val:(() => "") & { foo: number; }
  11. let ok: IOk = val; // This works
  12. let nok: INOk = val; // This does not

预期行为:

IOkINOk 具有相同的公共结构,它们都有一个调用签名且可索引,两个赋值语句都应该是有效的。

实际行为:

第二个赋值语句因消息 Index signature is missing in type '(() => "") & { foo: number; }'. 而失败。

**Playground 链接:**link
相关问题:#15300
备注

查看检查器代码,它似乎对于 IOk 类型兼容性会检查交集类型的每个组成部分,因此我们将有:

  1. isRelatedTo (typeof val, IOk) =
  2. isRealtedTo(typeof val, () => "")) // == True, since val has a call signature
  3. ( // Above equal to
  4. isRelatedTo(() => "", () => "") // == True
  5. ||
  6. isRelatedTo({ foo: number; }, () => "") // == False but it's does not matter
  7. )
  8. &&
  9. isRealtedTo(typeof val, { [name: string] : number }) // = True since the { foo: number} part of typeof val has an inferable index (ie isObjectTypeWithInferableIndex( { foo: number} ) will return true)
  10. ( // Above equal to
  11. isRelatedTo(() => "", { [name: string] : number }) // == False but does not matter
  12. ||
  13. isRelatedTo({ foo: number; }, { [name: string] : number }) // == True,
  14. )

而对于 INOk,关系是直接检查的,因为 INOk 不能拆分为组成部分,我们有

  1. isRelatedTo (typeof val, INOk) =
  2. isRelatedTo(() => "", INOk) // == False INOk has index, but ()=> "" does not
  3. ||
  4. isRelatedTo({ foo: number; }, INOk) // == False, No compatible call signature

所以检查器回退到结构检查( recursiveTypeRelatedTo ),但这也失败了,因为在检查索引兼容性(在 indexTypesRelatedTo 内部)时,它决定 typeof value ( (() => "") & { foo: number; } )没有可推断的索引( isObjectTypeWithInferableIndex 因为交集类型没有 symbol 而返回 false,即使它有,推断索引检查的条件是该类型没有调用签名( !typeHasCallOrConstructSignatures(type) ))。

wxclj1h5

wxclj1h51#

提议的解决方案允许 $x_{isObjectTypeWithInferableIndex}$ 对于交集类型返回真值,如果任何交集成分具有可推断的索引.

$x_{dragomirtitian@1d7723a}$

mrzz3bfm

mrzz3bfm2#

我们讨论过这个问题,认为交集类型行为(赋值是OK的)是期望的行为。
不过在我们改变任何东西之前,我们想了解一下你是如何发现这个的——未简化的复现看起来是什么样子?

mlmc2os5

mlmc2os53#

这是在stackoverflow上发布的问题,OP没有提供关于他们用例的更多信息。

wn9m85ua

wn9m85ua4#

Just to expand on what's going on here
One rule is that a type T is assignable to the type X & Y if T is assignable to X and T is assignable to Y ; this is one of the first principles of intersection types. There's no direct reasoning about X & Y during this operation.
Another rule is that a type T is assignable to a type { [s: string]: U } if all the declared properties of T are assignable to UandT has no call or construct signatures. The second clause is there because a type like { [s: string]: Foo } is a common "map" type and we don't want bare functions (which have no properties at all) to be assignable to these maps simply because they don't have properties.
This leads us to a "consistency triangle" problem. We think the first rule is correct, and we think the second rule is correct, but we also think X & Y should generally behave the same as its "normalized" form, but doesn't.
A proposed change is that types with at least one property are not subject to the "... does not have call or construct signatures" rule. This would be reasonably straightforward and square the circle, so to speak, but we're not really going to make a change unless we first understand how someone noticed this in the first place. Sometimes people go out hunting for inconsistencies and this hunt usually turns up something; we'd rather spend our risk budget on "real" issues if possible.
TL;DR if anyone noticed this in real code please tell us what it looked like so we can understand the severity/priority of it.

qgelzfjb

qgelzfjb5#

@RyanCavanaugh 我发布了StackOverflow上@dragomirtitian提到的问题。我在现实世界中没有遇到这个问题;我正在进行与TypeScript相关的硕士论文研究,并试图更好地了解类型系统允许的赋值操作。
感谢您的详细评论。这真的很有用!

vsmadaxz

vsmadaxz6#

第二个子句存在是因为像 { [s: string]: Foo } 这样类型的Map是常见的,我们不希望仅仅因为它们没有属性而将裸函数(没有任何属性)分配给这些Map。

@RyanCavanaugh 我主张这里的根本原因是,除非它本身是一个兼容的Map类型,否则不应该隐式地将任何内容分配给通用的“Map”类型。Map类型等同于产生任意输入保证值的调用签名 - 大多数Map不符合这个条件(在JS中不允许覆盖索引器,所以唯一的选择是将类型定义为 T | undefined )。索引签名的赋值兼容性目前相当不一致,特别是考虑到 keyof 类型的赋值兼容性。这与等效的调用签名也不一致。

  1. type E = { };
  2. type F = { a: number };
  3. type G = { a: number, b: number };
  4. type I = { [key: string]: number };
  5. type KE = keyof E;
  6. type KF = keyof F;
  7. type KG = keyof G;
  8. type KI = keyof I;
  9. type CE = (key: KE) => number;
  10. type CF = (key: KF) => number;
  11. type CG = (key: KG) => number;
  12. type CI = (key: KI) => number;
  13. declare const e: E;
  14. declare const f: F;
  15. declare const g: G;
  16. declare const i: I;
  17. declare const ke: KE;
  18. declare const kf: KF;
  19. declare const kg: KG;
  20. declare const ki: KI;
  21. declare const ce: CE;
  22. declare const cf: CF;
  23. declare const cg: CG;
  24. declare const ci: CI;
  25. const accept = <T>(value: T) => {};
  26. accept<KE>(ke);
  27. accept<KE>(kf); // error
  28. accept<KE>(kg); // error
  29. accept<KE>(ki); // error
  30. accept<KF>(ke);
  31. accept<KF>(kf);
  32. accept<KF>(kg); // error
  33. accept<KF>(ki); // error
  34. accept<KG>(ke);
  35. accept<KG>(kf);
  36. accept<KG>(kg);
  37. accept<KG>(ki); // error
  38. accept<KI>(ke);
  39. accept<KI>(kf);
  40. accept<KI>(kg);
  41. accept<KI>(ki);
  42. // conclusion about assignment compatibility:
  43. // KE <: KF <: KG <: KI
  44. // -> transitive relationship
  45. accept<CE>(ce);
  46. accept<CE>(cf);
  47. accept<CE>(cg);
  48. accept<CE>(ci);
  49. accept<CF>(ce); // error
  50. accept<CF>(cf);
  51. accept<CF>(cg);
  52. accept<CF>(ci);
  53. accept<CG>(ce); // error
  54. accept<CG>(cf); // error
  55. accept<CG>(cg);
  56. accept<CG>(ci);
  57. accept<CI>(ce); // error
  58. accept<CI>(cf); // error
  59. accept<CI>(cg); // error
  60. accept<CI>(ci);
  61. // conclusion about assignment compatibility:
  62. // CI <: CG <: CF <: CE
  63. // -> transitive relationship
  64. // -> exact reverse of the `keyof` relationship
  65. accept<E>(e);
  66. accept<E>(f);
  67. accept<E>(g);
  68. accept<E>(i);
  69. accept<F>(e); // error
  70. accept<F>(f);
  71. accept<F>(g);
  72. accept<F>(i); // error
  73. accept<G>(e); // error
  74. accept<G>(f); // error
  75. accept<G>(g);
  76. accept<G>(i); // error
  77. accept<I>(e);
  78. accept<I>(f);
  79. accept<I>(g);
  80. accept<I>(i);
  81. // conclusion about assignment compatibility:
  82. // G <: F <: E <: I
  83. // I <:> E
  84. // -> not transitive relationship
  85. // -> inconsistent with keyof
  86. // -> inconsistent with equivalent call signature

编辑:添加了一些小注解。此外,虽然这个具体的例子在我编写尝试约束泛型的通用库时并没有出现在我的脑海中,但在我研究了这个问题之后,我意识到将泛型参数限制为具有索引签名的类型是错误的(尽管从调用者的Angular 来看这是正确的)。总的来说,我认为这是一个学习语言和获得直觉的大问题,而且这种不一致性也没有在任何地方得到记录。

编辑2:有文档记录 arrays act covariantly ,我想字符串索引签名是数组的扩展,所以可以理解Map也具有协变性,但在我看来,对于泛型场景,关键是 keyof 关系更重要。

展开查看全部
9avjhtql

9avjhtql7#

我认为这里的根本原因是,除非它本身是一个兼容的Map类型,否则不应将任何内容隐式分配给通用的“map”类型。

这确实曾经是行为,但由于反馈而发生了变化。我们收到了很多“错误报告”,因为人们编写了这样的代码:

  1. function checkAges(map: { [name: string]: number }) { ... }
  2. const x = {
  3. "bob": 32,
  4. "alice": 26,
  5. "eve": 42
  6. };
  7. // Error, wat, of course this should be valid!
  8. checkAges(x);

我们收到了足够的报告,以至于我们为这个问题创建了一个StackOverflow问题。旧的行为在纸面上似乎是正确的,但在实践中导致了许多不必要的摩擦。

4uqofj5v

4uqofj5v8#

我刚刚也测试了1.8版本,它将 IF / G 视为不相关的类型,两者之间既不能赋值兼容,也不能互相赋值。如果它们是单向兼容的(相对于索引而言是逆变的),那么可能就不会有这么多问题了?

  1. accept<E>(e);
  2. accept<E>(f);
  3. accept<E>(g);
  4. accept<E>(i);
  5. accept<F>(e); // error
  6. accept<F>(f);
  7. accept<F>(g);
  8. accept<F>(i); // error
  9. accept<G>(e); // error
  10. accept<G>(f); // error
  11. accept<G>(g);
  12. accept<G>(i); // error
  13. accept<I>(e); // error
  14. accept<I>(f); // error
  15. accept<I>(g); // error
  16. accept<I>(i);
  17. // conclusion about assignment compatibility:
  18. // G <: F <: E
  19. // I <: E
  20. // -> transitive relationship

此外,SO文章中提到的1.8版本的下一个版本(2.0)引入了 strictNullChecks ,可以解决这个问题——如果在存在这个标志的情况下,索引类型始终被认为是可选的(隐式联合 undefined ),那么这个问题可能就不存在了。也许将这种行为添加到标志中是有意义的——假设最惊讶于无法赋值的人不会打开严格的检查。我知道单独的标志是一个维护噩梦,但在这个例子中,索引类型破坏了类型系统的根本性,所以也许是有意义的?

展开查看全部

相关问题