为什么做类型体操
TypeScript类型体操是一种在TypeScript中运用复杂类型定义和操作来实现更强大和灵活类型检查的方法。学习类型体操有助于提升代码的健壮性和可维护性,捕捉更多的潜在错误,并使代码更加自文档化。
如果已经熟练使用 TypeScript
,请直接跳转类型体操问题分解 。耐心看完本文,相信 type-challenges 无所畏惧!
类型体操基础知识
基础类型
JavaScript:
number
, boolean
, string
, symbol
, object
, undefined
, bigint
, null
.
TypeScript:
tuple
, enum
, Interface
, 字面量类型
, unknown
, void
, any
, never
.
undefined
vs null
vs void
vs never
在 TypeScript 中,undefined
、null
、void
和 never
是四个特殊的类型,它们有各自的用途和意义。
undefined
1
undefined
表示未定义的值。当一个变量声明了但没有赋值、对象中没有的属性或未传递的可选参数,它的值就是 undefined
。通常用于检查变量是否已被初始化,但不建议主动赋值为 undefined
。
同时,由于 undefined
是全局对象的一部分,因此可能会被重写,存在安全隐患。许多大型框架通过使用 void 0
来判断 undefined
,以确保安全性。
|
|
null
null
表示空值,通常表示一个空的对象引用。常用于释放对象或表示一个变量目前没有值。与 undefined
不同,null
是一个赋值给变量的值,表示变量已经被明确设置为空。
|
|
使用上述方式释放对象,一定要确保对象被释放后不会再被使用!!!
void
void
意味着函数的返回值不会被观察到。
此外,void
也可以用来声明只允许赋值 undefined
的变量,但这种用法较为罕见。
在 JavaScript
中,void
操作符用于计算一个表达式但不返回任何值,通常用于立即执行函数表达式(IIFE)或确保返回 undefined
。
|
|
never
never
表示永不存在的值的类型、不应到达的代码路径,通常用于会抛出错误或无限循环的函数。
|
|
总结与应用
undefined
:表示变量未初始化,可用于可选参数和可选属性。null
:表示有意设置为空的值,可用于初始化对象为空。void
:主要用于函数没有返回值的情况。never
:用于不应到达的代码路径,增强类型检查的完整性。
详见:any
, unknown
, object
, void
, undefined
, null
, and never
assignability
any | unknown | object | void | undefined | null | never | |
---|---|---|---|---|---|---|---|
any | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | |
unknown | ✓ | ✕ | ✕ | ✕ | ✕ | ✕ | |
object | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
void | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
undefined | ✓ | ✓ | ⍻ | ✓ | ⍻ | ✕ | |
null | ✓ | ✓ | ⍻ | ⍻ | ⍻ | ✕ | |
never | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
null
以及 undefined
在非严格检查下(strictNullChecks
off),会被作为其他类型的子类(除 never
)。
interface
vs type
在 TypeScript 中,interface
和 type
都可以用于定义类型,但有一些区别:
相同点:
- 都可描述对象结构和扩展类型。
区别:
- 声明合并:
interface
支持多次声明同名接口并合并,type
不支持。 - 类型别名:
type
可声明基本类型、联合类型、元组等,interface
只能声明对象类型。 - 高级类型操作:
type
支持类型运算,interface
不支持。 - 实现:类可
implements
接口,但不能implements
类型别名。 - 扩展内置对象:
interface
可扩展内置对象,type
不行。
使用建议:
- 用
interface
:需要声明合并、类实现、扩展内置对象时。 - 用
type
:定义基本类型别名、联合类型、元组、类型运算时。
总结:
interface
适合定义对象结构和接口规范。type
更灵活,可定义任意类型。
基础运算
后续的类型体操就依靠这些啦!!!
条件类型
条件类型是根据类型的条件来选择不同的类型。语法为【T extends U ? X : Y
】,表示如果 T
能赋值给 U
,则类型为 X
,否则为 Y
。
|
|
成立条件如下:
- 字面量及其原始类型,例如
1 extends number
。 - 结构化类型系统判断得到的子类型关系(包含派生)。
- Top Type(any, unknown) 与 Bottom Type(never)。
- 联合类型及其分支,例如
'a' | 'b' extends 'a' | 'b' | 'c'
。 - 分布式条件类型,见联合类型
类型约束
类型约束用于限制泛型类型的范围。语法为【T extends U
】,表示类型 T
必须是类型 U
的子类型。这个是后续类型体操的基础!!
|
|
类型推导
【infer
】关键字用于在条件类型中推导类型变量。它允许我们在条件类型的 extends
子句中引入一个新的类型变量。
|
|
使用 infer
做类型推断时,同一个候选值能有多个推断位置。
当 infer
处于协变位置时,结果为交叉类型。
For a given infer type variable
V
, if any candidates were inferred from co-variant positions, the type inferred forV
is a union of those candidates. Otherwise, if any candidates were inferred from contra-variant positions, the type inferred forV
is an intersection of those candidates. Otherwise, the type inferred forV
is never.
|
|
当 infer
处于逆变位置时,结果为联合类型。
|
|
联合类型
联合类型表示一个值可以是几种类型之一。使用【|
】符号来定义联合类型。联合类型进行条件运算时,会对每个成员进行分布式处理,这种行为被称为“分布式条件类型”。
|
|
泛型中,对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上。
Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of
T extends U ? X : Y
with the type argumentA | B | C
forT
is resolved as(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
.
交叉类型
交叉类型表示一个值可以同时是几种类型。使用【&
】符号来定义交叉类型。
|
|
索引查询
索引查询操作符【keyof
】用于获取某个类型的所有键,返回一个联合类型。关于 keyof (A & B)
的计算详见链接
|
|
索引访问
索引访问操作符【T[K]
】用于获取某个类型的特定属性的类型。
|
|
索引遍历
【in
】用于遍历一个类型的所有键,并生成一个新的类型。常见的用法是结合 keyof
操作符来获取类型的所有键。
|
|
索引重映射
【as
】关键字用于在映射类型中重映射键。
|
|
修饰符
- readonly:将属性设置为只读,不能被重新赋值。
- ?:将属性设置为可选。
- +/-:从类型中添加/移除属性修饰符。
|
|
断言
- as const
- satisfies
as const
用于将变量或对象的类型断言为最严格的字面量类型。这在定义不可变数据时非常有用,特别是在定义常量对象或数组时。
|
|
用法:
- 使用
as const
可以在类型层面确保数据的不可变性。 - 常用于定义枚举类型的取值集合。
satisfies
它可以确保一个表达式满足某个类型约束,而不改变其被推断的类型。
|
|
工具类型
这些工具的详情可以查看 es5.d.ts;
常用类型工具
Partial
, Required
, Readonly
, Pick
, Record
, Exclude
, Extract
, Omit
, NonNullable
, Parameters
, ConstructorParameters
, ReturnType
, InstanceType
字符串映射工具
Uppercase
, Lowercase
, Capitalize
, Uncapitalize
进阶类型工具
NoInfer
上下文类型工具
ThisType
, 这个在类型体操里面用的很少,主要在实战场景。
小技巧
keyof any
keyof any
的结果类型是 string | number | symbol
,表示所有可能的对象键类型。这在需要泛型键类型时非常有用。
|
|
类型交叉 (string & {})
交叉类型 (string & {})
可以将基础类型转化为非原始类型,从而触发类型兼容性的细微差别。常用于防止泛型参数过于宽泛,确保类型的精确性。
|
|
Brand Type
用于在类型系统中创建具有独特标识的类型,防止不同类型之间的混用。通过在类型中添加一个独特的属性(通常是私有的符号或唯一的字符串字面量),可以使逻辑上相同的类型在类型系统中被视为不同的类型。
|
|
在编译后不会影响生成的 JavaScript 代码,只在类型检查阶段生效。代码中不要对类型标志做任何赋值访问!!
协变与逆变
协变(Covariance)
和 逆变(Contravariance)
是与类型兼容性和子类型关系相关的重要概念。
先约定如下的标记:
- 【
A ≦ B
】意味着 A 是 B 的子类型。 - 【
A → B
】指的是以 A 为参数类型,以 B 为返回值类型的函数类型。 - 【
C<A>
】泛型。
解释名词:
协变
:如果它依然保持子类型序关系。A ≦ B
,则ReadonlyArray<A> ≦ ReadonlyArray<B>
,() → A ≦ () → B
。逆变
:如果它逆转了子类型序关系。A ≦ B
,则B → void ≦ A → void
。不变
(invariant):如果上述两种均不适用。
对于函数类型,我们可以总结为: 对输入类型是逆变的而对输出类型是协变的。
思考一个场景:A ≦ B
,则 B[] → void ≦ A[] → void
。这个是否合理呢?显然是不合理的,因为可以传入 A[]
,但是可以在子类型 B[] → void
的代码中插入一个 B
到 A[]
上。那么 A[]
的内容就类型错误了!
给一个类似的简化代码:
|
|
由于没有类似 Dart
中的 convariant
关键字,TypeScript
对函数参数采用的时候双向可变的方案。详见 Why are function parameters bivariant?
参考文献
- [Web Dev] null and undefined
- 业务代码里的 TypeScript 小技巧
- Microsoft Typescript FAQ
- Covariance and contravariance (computer science)
TypeScript
官方的代码风格中要求使用undefined
,这个因团队而异。TypeScript
官方也不推荐使用const enum
,但是代码里面也到处飞啊! ↩︎