我熟悉面向对象编程、联合类型和协变/逆变,但对类型脚本相对较新.

我继承了一个旧项目(TypeScrip3.2),代码如下:

class A {
  public foo: string
}

class B extends A {
  public bar: string
}

function foo(obj: A | B) {
  return obj.foo
}

const array: (A | B)[] = [new A(), new B()]

在代码库中有数百个位置使用A | B.据我所知,这是完全多余的,因为B的任何实例也是A的实例.函数和数组在类型脚本中是协变的,所以为什么不这样做呢?它更简单,编译起来也很好:

function foo(obj: A) {
  return obj.foo
}

const array: A[] = [new A(), new B()]

我是不是遗漏了什么?有没有什么情况下这是有用的?

推荐答案

如果Typescript 的排版系统是完全正确的,那么无论何时X extends Y,union X | Y应该减少到Y,也就是众所周知的subtype collapsing.(同样,intersection X & Y应该减少到X.这些都是absorption laws的表格.)

根据特定的XY类型,这does发生在Typescript 中.具体地说,基元类型大多是这样的(例如,"abc" | string减少到string123 & number减少到123).但TypeScrip的打字系统并不完全正确(也不打算完全正确,参见TS Design Non-Goal #3),而且在某些情况下,方便胜过正确.

关于全面实施吸收定律的旧建议,请参见microsoft/TypeScript#16386,该定律最终作为部分固定和部分拒绝而关闭.


A | B这样的对象类型往往不会减少,因为更具体的类型往往具有在吸收后会完全忘记的特性.(对于B,这是bar属性.)至少,这对于文档来说不是很好.即使在完全健全的类型系统中,人们也可能希望在他们的文档中使用see A | B,而不是A.

此外,一些方便但在技术上不正确的类型脚本构造do关心这些属性,因此在AA | B之间存在明显的差异.


其中一个这样的构造是using the in operator to distinguish between union members.对于A | B,您可以使用"bar"作为关键字,以将范围缩小到B:

function f(obj: A | B) {
    if ("bar" in obj) {
        obj.bar.toUpperCase() // okay
    }
}

但只要花A英镑,你就不能:

function f2(obj: A) {
    if ("bar" in obj) {
        obj.bar.toUpperCase() // error
    }
}

因此,A | B保留了编译器用来执行控制流分析的信息.当然,这非常有用,但在技术上是不正确的,因为它不安全:

class Z extends A {
    public bar: number = 1
}
f(new Z()); // <-- accepted but explodes

但是,as mentioned in microsoft/TypeScript#15256, the pull request implementing in operator narrowing,效用超过了不正确:

现实情况是,大多数unions 已经正确地脱节,没有足够的混叠来体现这个问题.编写in测试的人不会写出"更好"的判断;实际发生的情况是人们添加类型断言或将代码移到同样不可靠的用户定义类型谓词中.


另一方面,这是另一个这样的 struct :excess property checking.当您将值object literal赋给一个不期望特定属性存在的位置时,您将收到一个错误,警告您编译器将完全忘记该属性,您可能犯了一个错误:

interface C { c: 1 }
const c: C = { c: 1, d: 2 }; // error
// ----------------> ~
// Object literal may only specify known properties, 
// and 'd' does not exist in type 'C'.

这很方便,大多数人都想要这个错误,但从技术上讲,这是一个错误的警告,因为{c: 1, d: 2}is可以赋给C.多余的属性判断将incompleteness引入到不需要存在的语言中.

因此,以下是unions 可能会有所帮助的地方:

interface D extends C { d: 2 }
const cd: C | D = { c: 1, d: 2 }; // okay

类型C | D不会减少到C,因此看不到{c: 1, d: 2}具有任何过剩属性.这是in运算符缩小的另一面,因为d不是多余属性的原因是因为您可以通过in运算符缩小来恢复其类型.


多余的属性判断与in运算符缩小本质上类似于另一种类型系统,其中类型脚本对象类型被关闭/密封/exact(如microsoft/TypeScript#12936中所要求的).在那个世界里,类型将prohibit任何额外的属性.因此B extends AD extends C将仅意味着类/接口层次 struct ,而不是type层次 struct .意思X extends Y不再意味着XY的一个子类型,因此X | Y不再等同于Y.因此,在某种意义上,对象联合的子类型减少的缺乏是为了允许部分类型脚本生活在精确类型的另一个世界中.

Playground link to code

Typescript相关问答推荐

我可以让Typescript根据已区分的联合键缩小对象值类型吗?

角形标题大小写所有单词除外

Typescript泛型类型:按其嵌套属性映射记录列表

Express Yup显示所有字段的所有错误

在TypeScrip的数组中判断模式类型

使用TypeScrip根据(可选)属性推断结果类型

如何表示多层深度的泛型类型数组?

TypeScrip原始字符串没有方法

具有自引用属性的接口

vue-tsc失败,错误引用项目可能无法禁用vue项目上的emit

转换器不需要的类型交集

在React中定义props 的不同方式.FunctionalComponent

专用路由组件不能从挂钩推断类型

ANGLE订阅要么不更新价值,要么不识别价值变化

TaskListProps[]类型不可分配给TaskListProps类型

可赋值给类型的类型,从不赋值

使用强制转换编写打字函数的惯用方法

基于区分的联合键的回调参数缩小

T的typeof键的Typescript定义

React-跨组件共享功能的最佳实践