TS 在语法上是 JS 的超集:
“TypeScript 是 JavaScript 的超集”
“TypeScript 是带类型的 JavaScript ”
能够保证静态类型准确性的类型系统被称为 sound 的(类型安全)。
TS 的类型系统非常不 sound,这也不是 TS 的设计目标。
这一段代码并不会产生运行错误,但 TS 仍然标出类型问题
显式声明预期的数据类型是 “State” 这一结构,TS 错误提示会更准确
TS 如何判断某种用法是否是 “奇怪的用法”?
说到底是编程风格的偏好。使用 TS 就是选择了开发 TS 的团队的偏好。
instanceof 检查发生在运行时,但 Rectangle 是一个 TS 类型,它不能影响代码的运行时行为。
TS 类型是 "可擦除的":编译到 JS 时会从代码中删除所有的 interface、type 和其他类型声明。
as number 是 TS 类型操作,它不能影响代码运行时的行为
生成兼容版本的 JS 时可能存在性能开销,例如使用 generator 时编译到 ES5 要引入支持库,这与原生实现存在性能差距。但这也与类型无关,取决于使用的特性和目标编译版本。
TS 允许用一个 NamedVector 来调用 calculateLength,因为它的结构与 Vector2D 兼容。这就是 "结构类型化 "一词的由来。(下文会说明如何避免结构类型的问题)
与结构化类型对应的是名义类型 Nominal Typing,必须显式继承类或实现接口才被认为是该类型。
calculateLength 操作的是二维向量,而 normalize 操作的是三维向量。TS 并没有发现这个问题。
为什么可以用三维向量调用 calculateLength,尽管它的类型声明说它需要二维向量?
v 是 Vector3D,它的属性应该是 "x"、"y "或 "z",根据类型声明 axis 的值应该都是数字,但 TS 会提示错误
由于 v 可以有任何属性,所以 axis 的类型是 string。
TS 也没有理由相信 v[axis] 必定是一个数字,正如上图所示,它可能不是。
[number, number] 对应的是 {0: number, 1: number, length: 2}
[number, number, number] 对应的是 {0: number, 1: number, 2: number, length: 3}
除了要求数字下标对应的元素外,还有隐式的 length 属性要求
在 JS 运行时,每个变量都有一个单一值。这些值包括:
在初中数学课中学习过,描述集合的方式有两种:
如果值有一个 id 属性,且它可以赋值给一个 string,那么它就是 Identified 类型的。
1. 枚举集合内可能的值:
2. 描述符合集合内值的条件:
全集对应的类型是什么?
可以这样理解 “可赋值(assignable )”:
1. 一个值 assignable 一个类型,等价于 "一个值属于一个类型值的集合"
2. A 类型 assignable B 类型,等价于 "A 类型值的集合是 B 类型值的集合的子集":
所有 Vector3 构成的集合是所有 Vector2 构成的集合的子集,或者说所有 Vector3 都是 Vector2( Vector3 都 assignable Vector2)
可以这样理解 ”&“:A & B 代表 A 类型值的集合与 B 类型值的集合的交集
A & B 表示既在 A 的集合又在 B 的集合的所有值,这些值必然既具有 A 的属性,又具有 B 的属性
可以这样理解 ”extends“:A extends B 等价于 A 类型值的集合是 B 类型值的集合的子集
PersonSpan 中的每个值都必须有 name 属性,而且每个值都必须有一个 birth 属性。
"A 是 B 的子类型(subtype)" 等价于 ”A 类型的集合是 B 类型集合的子集“ 。
assignable, subset, extends 含义是等价的,都与 ”子集“ 是同义词
”集合“的方式更容易理解非继承关系的类型间的操作
注意:不是每一个集合都存在相应的 TS 类型,例如”所有整数组成的类型“
interface Cylinder 在类型空间声明了一个类型,const Cylinder 在值空间声明了一个变量,尽管名称相同,它们不会产生冲突
TS 中各类概念对应的空间
在 Typescript Playground 中多尝试,查看标识符的含义,比较编译前后的结果,可以快速掌握值与类型的区别。
其中,最容易引起困惑的是 typeof。
TS 提供了工具类型从构造函数类型获取实例类型
也可以自行声明从实例类型获取构造函数类型的工具类型:
属性访问运算符 [] 也同时存在于两个空间
当将一个对象字面量赋值给一个声明了类型的变量时,TypeScript 确保它具有该类型的属性,而且没有其他属性。这就是 “多余属性检查”。
这似乎与 TS 结构化类型行为不一致,因为 r 拥有 Room 所需的全部属性
如何理解和利用 TS 的这种行为?
通过引入中间变量,可以绕过上述检查。
在第一个例子中,触发了 TS 的 “多余属性检查”的过程,它可以检测出结构化类型系统通常会忽略的一类错误。
多余属性检查
TS 为什么要这样设计?
虽然不会导致运行时错误,但 TS 认为某些奇怪的用法更可能是错误而不是开发人员的真实意图,因此也会发出警告或错误
额外的属性大多数时候都不是开发者的预期行为(拼写错误等),TS 添加额外属性检查机制来检查这种错误
额外属性检查与结构化类型的原则向矛盾,但可以理解 TS 设计者为了开发体验的一种平衡设计。
上面的例子在类型名称前缀中使用了 I 或 T,仅是为了区分其定义。实际代码中不应该这样做。在 TS 的早期阶段,这种约定有所应用。但现在一般被认为是不好的风格,因为是不必要的。
大多数情况下,type 与 interface 都可以实现相同功能,遵守团队约定即可
声明合并在标准库类型声明中非常常见:不同版本的 ES 中都会声明 interface Array,并向其中加入额外的数值属性/方法
看起来相似的两种声明方式,在异常情况表现不同
在需要显式标注类型时,除非有特殊原因,总是应该优先使用类型声明
使用 reduce 的泛型显式标注类型,更不容易出错
对于常用的数据结构,可以创建自定义 type guard。
相比类型断言,能严格保证类型与运行时的值一致。
使用 filter 时,TS 不能推断出筛选后的类型。
在你确实比 TS 更了解某个类型的情况时。一般是因为这种情况下你拥有类型检查器无法获知的上下文信息。(必须确保类型与值是一致的。不要欺骗 TS!)
由于 TS 类型检测器无法访问页面的 DOM,它无法知道 `#myButton` 是一个按钮元素。
类似的,也可以通过 ! 强制指定类型为非空的
有时由于语言能力限制,必须使用类型断言。
可以将这段逻辑包装在一个函数内部,只要保证对外的接口类型是准确的,那它的影响面是可以接受的。(一般来说这种场景的代码改动频率较低)
| 基础类型 | 包装类型 |
|---|---|
| string | String |
| number | Number |
| boolean | Boolean |
| symbol | Symbol |
| bigint | BigInt |
在 JS 中,基础值存在包装对象的概念。TS 为了兼容这一行为,每一种基础类型都有对应的包装类型。
基础类型可以赋值给对应的包装类型,但反过来不行。
实际使用中,几乎没有需要使用包装类型的场景。
为了一致性,请一律使用基础类型。
在编程中,重复代码会造成维护性问题,我们会通过提取变量,函数的方式避免重复。
TS 类型也是一样,需要尽量避免重复声明相同类型
对于确定一致的结构,应该使用命名类型来复用。这与将重复的字面量提取出来作为常量是一样的思路。
使用 extends 扩展命名类型得到新类型
使用 Pick 泛型从命名类型中裁剪得到新类型
泛型就如同类型的函数,熟练掌握可以避免重复声明类型。
不过,从值生成类型时要小心。一般最好先定义类型,再声明值的类型。这样可以使类型更明确,不会被 TS 从值推断出的类型干扰。(下文会介绍自动推断的规则)
使用 Mapped Type 保证类型一致性
假设 React 组件有这样的 props,我们希望在除了 onClick 之外的 props 变化时才 rerender
[k in keyof ScatterProps] 告诉 TS,REQUIRES_UPDATE 需要与 ScatterProps 有相同的 key,一旦它们不同步,TS 会报错。
其他需要保证一致性的场景下,也可以建立这样的类型关系,让 TS 帮助检查。
在大多数时候,TS 能够根据上下文自动推导出变量的类型。额外的类型标注反而会影响代码的维护性。
当某一天,Product.id 从 number 改成 string 时,额外的类型标注会产生不必要的错误
下文会说明 TS 自动推导的原则,以及如何修改推导出的类型
1. 声明对象字面量时
2. 函数的返回值(特别是模块对外的公开函数)
TS 认为:一个变量的值可以改变,但是它的类型一般不应该改变
这同样是 TS 设计者的偏好,这样做一般不会有正面作用,是应该避免的做法。
思考:TS 为什么推导成 string,而不是更严格的 "12-34-56"?
为什么 TS 将 x 的类型推断为 string 而不是 "x"?
v.x 的类型与 let x = 1 的类型相同
TS 通过 if (el) 分析出两个分支下的类型可以更精确
尽量使用单个变量访问同个值可以帮助 TS 追踪类型
typeof 的作用类似,常用于收窄为 string, number 等基本类型
这种做法叫做 "tagged union"
只要 isInputElement 返回 truthy 值,el 类型就会被 TS 收窄为 HTMLInputElement
EGS 提供的 type guard 工具函数
类似地,在类的构造函数中要确保初始化所有声明的属性
始终要保证值与类型一致
如何理解 isLoading 为 true,error 又非空的状态?程序逻辑中要如何处理?
加载中时,isLoading 为 true,加载结束后 isLoading 为 false。当加载出错时,error 为错误信息,成功时 error 为空
当类型只能表达合法的状态,TS 通过类型验证业务状态和逻辑都是合法的。
出生地和初始日期要么都有,要么都没有。第二种定义更合理。
尽可能精准地描述类型,并且保证值与类型一致,可以让 TS 类型检查发挥最大作用。
给类型声明特有的属性,阻止外界轻易创建出兼容结构的数据,从而实现只接受来自特定实现的数据。
使用特殊属性名或值,例如用 symbol 作为值或属性可以有更强的保证
提供公共包时,使用 dts-rollup 对编译出的 .d.ts 打包,可以控制对外暴露的接口,避免内部接口泄漏,影响项目后续维护。
公司内其他同事分析的最佳实践:
谢谢