TypeScript 版本: 2.9.0-dev
搜索词: index signature intersection
代码
type INOk = {
(): string;
[name: string] : number;
}
type IOk = {
(): string;
} & {
[name: string] : number;
}
declare let val:(() => "") & { foo: number; }
let ok: IOk = val; // This works
let nok: INOk = val; // This does not
预期行为:
IOk
和 INOk
具有相同的公共结构,它们都有一个调用签名且可索引,两个赋值语句都应该是有效的。
实际行为:
第二个赋值语句因消息 Index signature is missing in type '(() => "") & { foo: number; }'.
而失败。
**Playground 链接:**link
相关问题:#15300
备注
查看检查器代码,它似乎对于 IOk
类型兼容性会检查交集类型的每个组成部分,因此我们将有:
isRelatedTo (typeof val, IOk) =
isRealtedTo(typeof val, () => "")) // == True, since val has a call signature
( // Above equal to
isRelatedTo(() => "", () => "") // == True
||
isRelatedTo({ foo: number; }, () => "") // == False but it's does not matter
)
&&
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)
( // Above equal to
isRelatedTo(() => "", { [name: string] : number }) // == False but does not matter
||
isRelatedTo({ foo: number; }, { [name: string] : number }) // == True,
)
而对于 INOk
,关系是直接检查的,因为 INOk
不能拆分为组成部分,我们有
isRelatedTo (typeof val, INOk) =
isRelatedTo(() => "", INOk) // == False INOk has index, but ()=> "" does not
||
isRelatedTo({ foo: number; }, INOk) // == False, No compatible call signature
所以检查器回退到结构检查( recursiveTypeRelatedTo
),但这也失败了,因为在检查索引兼容性(在 indexTypesRelatedTo
内部)时,它决定 typeof value
( (() => "") & { foo: number; }
)没有可推断的索引( isObjectTypeWithInferableIndex
因为交集类型没有 symbol
而返回 false,即使它有,推断索引检查的条件是该类型没有调用签名( !typeHasCallOrConstructSignatures(type)
))。
8条答案
按热度按时间wxclj1h51#
提议的解决方案允许 $x_{
isObjectTypeWithInferableIndex
}$ 对于交集类型返回真值,如果任何交集成分具有可推断的索引.$x_{dragomirtitian@1d7723a}$
mrzz3bfm2#
我们讨论过这个问题,认为交集类型行为(赋值是OK的)是期望的行为。
不过在我们改变任何东西之前,我们想了解一下你是如何发现这个的——未简化的复现看起来是什么样子?
mlmc2os53#
这是在stackoverflow上发布的问题,OP没有提供关于他们用例的更多信息。
wn9m85ua4#
Just to expand on what's going on here
One rule is that a type
T
is assignable to the typeX & Y
ifT
is assignable toX
andT
is assignable toY
; this is one of the first principles of intersection types. There's no direct reasoning aboutX & Y
during this operation.Another rule is that a type
T
is assignable to a type{ [s: string]: U }
if all the declared properties ofT
are assignable toU
andT
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.
qgelzfjb5#
@RyanCavanaugh 我发布了StackOverflow上@dragomirtitian提到的问题。我在现实世界中没有遇到这个问题;我正在进行与TypeScript相关的硕士论文研究,并试图更好地了解类型系统允许的赋值操作。
感谢您的详细评论。这真的很有用!
vsmadaxz6#
第二个子句存在是因为像 { [s: string]: Foo } 这样类型的Map是常见的,我们不希望仅仅因为它们没有属性而将裸函数(没有任何属性)分配给这些Map。
@RyanCavanaugh 我主张这里的根本原因是,除非它本身是一个兼容的Map类型,否则不应该隐式地将任何内容分配给通用的“Map”类型。Map类型等同于产生任意输入保证值的调用签名 - 大多数Map不符合这个条件(在JS中不允许覆盖索引器,所以唯一的选择是将类型定义为
T | undefined
)。索引签名的赋值兼容性目前相当不一致,特别是考虑到keyof
类型的赋值兼容性。这与等效的调用签名也不一致。编辑:添加了一些小注解。此外,虽然这个具体的例子在我编写尝试约束泛型的通用库时并没有出现在我的脑海中,但在我研究了这个问题之后,我意识到将泛型参数限制为具有索引签名的类型是错误的(尽管从调用者的Angular 来看这是正确的)。总的来说,我认为这是一个学习语言和获得直觉的大问题,而且这种不一致性也没有在任何地方得到记录。
编辑2:有文档记录 arrays act covariantly ,我想字符串索引签名是数组的扩展,所以可以理解Map也具有协变性,但在我看来,对于泛型场景,关键是 keyof 关系更重要。
9avjhtql7#
我认为这里的根本原因是,除非它本身是一个兼容的Map类型,否则不应将任何内容隐式分配给通用的“map”类型。
这确实曾经是行为,但由于反馈而发生了变化。我们收到了很多“错误报告”,因为人们编写了这样的代码:
我们收到了足够的报告,以至于我们为这个问题创建了一个StackOverflow问题。旧的行为在纸面上似乎是正确的,但在实践中导致了许多不必要的摩擦。
4uqofj5v8#
我刚刚也测试了1.8版本,它将
I
和F
/G
视为不相关的类型,两者之间既不能赋值兼容,也不能互相赋值。如果它们是单向兼容的(相对于索引而言是逆变的),那么可能就不会有这么多问题了?此外,SO文章中提到的1.8版本的下一个版本(2.0)引入了
strictNullChecks
,可以解决这个问题——如果在存在这个标志的情况下,索引类型始终被认为是可选的(隐式联合undefined
),那么这个问题可能就不存在了。也许将这种行为添加到标志中是有意义的——假设最惊讶于无法赋值的人不会打开严格的检查。我知道单独的标志是一个维护噩梦,但在这个例子中,索引类型破坏了类型系统的根本性,所以也许是有意义的?