00. 长话短说
大家好,我是大家的林语冰~ 👨💻
TS 支持 3 种通用对象类型,类型系统允许它们互相赋值,但一般不推荐直接使用。
在本文中,我们会结合 TS 源码,深度学习这 3 种通用对象类型的设计动机和技术细节:
- 为什么原始值允许赋值给大写的
Object类型 - 为什么 TS 团队又引入了小写的
object类型 - 如何理解
{}字面量类型
01. Object 类型
大写的 Object 类型和 JS 内置的 Object() 函数同名,所以比较迷惑。
现实开发中,我们只需要区分编程动机,Object 是作为类型注解(TS 接口),还是作为函数调用(JS 函数),就不会混淆。
⛔ 注意,虽然两者的拼写一模一样,但 Object() 其实不是 Object 类型。
我们可以结合 TS 源码来理解:
可以看到,Object() 函数的类型其实是 ObjectConstructor 接口,而实例对象 o 和原型对象 prototype 才是 Object 类型。
我们继续阅读 TS 源码中 Object 接口的定义:
可以看到,Object 接口定义了 Object.prototype 原型对象上能被继承的所有 公共属性,比如 ({}).__proto__.toString() 方法等。
这意味着,几乎 所有 Object() 构造函数的实例对象和子类实例都允许赋值给 Object。
⛔ 注意,JS 中存在一种“裸对象”极端情况 —— 无原型对象:
可以看到,虽然无原型对象不是 Object() 的实例,但它也允许赋值给 Object 类型。
从技术上讲,TS 允许所有 typeof x === 'object' 断言为真的对象赋值给 Object 类型(typeof null 除外),它们大都能够上溯原型链访问 Object.prototype。
另一点比较反直觉的是,TS 还允许 非空原始值 赋值给 Object 类型。
这是因为,作为动态类型语言,JS 允许 非空原始值 像对象一样直接调用方法,这通过 包装对象 来实现:
- 当原始值调用方法时,比如字符串
'str'.slice(),JS 引擎会在底层将'str'自动装箱 为Object('str'),将原始值强转为 包装对象; 'str'.slice()方法调用完成后,JS 引擎又会将包装对象 自动拆箱,强转为原始值。
因此,为了兼容 JS 的 装箱/拆箱 机制,TS 就将静态类型系统的设计对齐 包装对象 的运行时行为:
上述代码中,str 手动注解为 Object 类型后,str 的类型既不是 string,也不是 String。
由于 Object 接口并没有定义 ''.slice() 方法,所以 (str as Object).slice() 会报错,需要我们使用类型断言手动修正。
⛔ 注意,TS 团队不推荐使用 Object 类型,永远禁止滥用大写的 String 和 Object 等包装对象类型!
同理可得,虽然 null 和 undefined 也是 JS 原始值,但它们没有对应的包装对象,所以禁止赋值给 Object 类型。
从技术上讲,Object 接口可以理解为 any 和 unknown 之外的第 3 种 顶端类型,除了 null 和 undefined 之外的所有类型都是 Object 类型的子类型。
换而言之,TS 允许任意 非空值 赋值给 Object 类型。
02. object 类型
根据《ECMAScript 语言规范》,JS 的数据类型可以分为 2 种对立类型:
可以看见,JS 的原始类型和对象类型(非原始类型)构成一个完整的类型全集,它们互补互斥。
但由于 TS 的 Object 接口也兼容原始类型,TS 早期缺少可以严格表示非原始类型的对象类型。
因此,TS 2.2 引入了小写的 object 类型,与 Object 接口不同,它专门用于表示 非原始类型:
可以看到,TS 禁止原始值赋值给 object 类型。此外,object 也 禁止赋值 给大多数对象字面量类型。
⛔ 注意,object 类型 能且仅能 赋值给下列 4 种类型:
any类型unknown类型Object接口{}空对象字面量类型
虽然 object 类型比 Object 接口更严格,但它并非没有用武之地,我们可以偷师 TS 源码的最佳实践:
根据《ECMAScript 语言规范》,Object.create() 的方法签名严格要求第一个参数必须是对象或者 null。
TS 2.2 之前,TS 能且仅能 使用万能的 any 作为次优解,副作用是会丢失部分类型细节。
TS 2.2 之后,这种场景下,使用 object 类型是最优解,我们可以得到精准的类型提示。
03. {} 字面量类型
{} 空对象字面量类型和 {} 空对象字面量一模一样,它是对象字面量类型的特例,表示没有任何自有属性的对象。
可以看到,{} 空对象字面量类型的赋值兼容性基本和 Object 接口一致。
由于 {} 空对象字面量类型 有且仅有 继承自 Object.prototype 的原型属性,所以它偶尔可以用作 Object 接口的备胎。
04. 对象类型相对论
从技术上讲,Object 类型和 {} 字面量类型都兼容原始类型,所以它们表面上比 object 类型更宽松。
但当对象重写继承自 Object.prototype 的属性签名时,Object 类型会更严格:
根据《ECMAScript 语言规范》,Object.prototype.toString() 方法签名 能且仅能 返回 string 类型。
因此,当目标对象的属性签名和 Object.prototype 的类型不兼容时,TS 就会报错。
而 object 类型和 {} 字面量类型并没有明确定义目标对象的属性签名,所以 TS 不会检查类型兼容性。
05. 对象类型基本法
刚入门 TS 时,我们容易滥用或误用通用对象类型:
可以看到,通用对象类型没有明确目标对象的属性签名,所以 VSCode 等 IDE 可能没有拼写提示或代码补全,TS 编译时也会报错,这就完美规避了静态类型的所有优势。
⛔ 注意,滥用通用对象类型就像 写了一句无效的注释,对特定对象使用通用的类型注解没有实际意义,我们要有意规避这种 反模式。
相反,我们推荐下列 3 中常见的替代方案:
高潮总结 👇
TS 支持 3 种通用对象类型,一般不推荐直接使用,它们的设计动机有所不同:
Object类型 - 它表示Object.prototype的接口,且兼容原始类型object类型 - 它表示非原始类型,禁止原始值赋值{}空对象字面量类型 - 它和Object接口大同小异,但不检查对象属性签名的兼容性
参考文献
- TS GitHub:github.com/microsoft/T…
- Do's and Don'ts:www.typescriptlang.org/docs/handbo…
- TS@2.2:www.typescriptlang.org/docs/handbo…
粉丝互动 😍
👉 本期话题是:你是否遭遇过误用 TS 通用对象类型而导致的 bug?🤣
读完了的小伙伴可以在本文下方留言互动,或者友情转发。👍
我是大家的林语冰 👨💻,欢迎持续关注我,深度学习前端进阶的技术细节。谢谢大家的点赞和转发,我们下期再见,掰掰~ 👻