有时为了简化具有复杂输入的函数签名,我们会定义一个“已知”复杂输入的集合,每个输入都与一个令牌相关联,并允许用户提供令牌。
我的函数适用于元组(这里简化为 3 元素元组),最初它没有任何编译器错误:
const TUPLE_LENGTH = 3
type Tuple = readonly [unknown, unknown, unknown]
function fn(tuple: Tuple): void {
if (tuple.length !== TUPLE_LENGTH) {
throw new Error(`Expected ${TUPLE_LENGTH} items, instead got ${tuple.length}`)
}
}
尝试一下。
…但是在添加“已知”输入后,开始出现编译器错误:
const TUPLE_LENGTH = 3
type Tuple = readonly [unknown, unknown, unknown]
const knownTuples = {
foo: [1, 2, 3],
bar: ['a', 'b', 'c'],
baz: [true, false, null],
} satisfies Readonly<Record<string, Tuple>>
type TupleName = keyof typeof knownTuples
function fn(tuple: Tuple | TupleName): void {
if (typeof tuple === 'string') {
tuple = knownTuples[tuple]
}
if (tuple.length !== TUPLE_LENGTH) {
throw new Error(`Expected ${TUPLE_LENGTH} items, instead got ${tuple.length}`)
// ^^^^^^
// Error: Property 'length' does not exist on type 'never'
}
}
尝试一下。
由于某种原因,if (tuple.length !== TUPLE_LENGTH)
在第一种情况下,它是 ,tuple: Tuple
但在第二种情况下,它缩小到tuple: never
。这是为什么?
TypeScript 主要只对联合类型执行缩小。如果是联合类型,则第一次检查会排除这种可能性(因为它不是),第二次检查也会排除这种可能性(因为它的长度不是,并且所有的长度都是)。因此,会一直缩小到不可能的类型,并且在尝试进一步与其交互时会出错。这些错误告诉您代码流无法到达有问题的代码块。
tuple
Tuple | TupleName
TupleName
string
Tuple
3
Tuple
3
tuple
never
但是当
tuple
是非联合类型时Tuple
,不会发生这种缩小,即使从逻辑上讲您仍然排除了所有可能性,并且控制流无法到达有问题的代码锁。您的问题是:为什么?答案是,这是 TypeScript 的设计决策,详见microsoft/TypeScript#38963。
最大的原因是,如果对所有值都进行缩小,编译器性能将受到严重影响,而且好处很少。虽然
never
类型一致性会提高一些,但大多数情况下,类型检查器会做大量额外的工作来缩小值,而几乎没有人会尝试缩小值。第二个原因是,显然有很多非联合类型,人们会执行不必要的额外运行时检查(就像您在第一个示例中所做的那样),而这些错误会让人烦恼。是的,正如您所展示的,联合也可能发生这种情况,但实际上这种情况并不常见。那些费尽心思将其值建模为联合类型的人通常意味着这些值无需运行时检查即可正确输入。
是的,有人可能会争辩说,为了保持一致性,它应该是两者兼而有之或两者都不是,但一致性并不是 TypeScript 的最终目标。请参阅microsoft/TypeScript#9825 上的这条评论(以及整个问题)。现实世界代码的易用性有时被认为比一致性更重要。这必须通过经验和统计来衡量;人们多久会遇到一次这样的问题,如果我们改变它,我们会让平均体验更好还是更糟?在这种情况下,当前的行为“联合缩小,非联合不缩小”对于广泛的现实世界代码来说已经足够好了,而使其保持一致会损害普通用户,原因如上所述。
由于您不能发送长度不同于 3 的元组,因此该代码已经是类型安全的。因此第二种类型保护实际上是
tuple
由类型构成的,never
因此错误是合法的。您不需要此类型保护,它没有意义(TS 会通过错误有效地显示这一点),只需将其删除即可。否则,您需要允许将数组而不是 tuple[3] 传递给函数。