TypeScript 的另一面:类型编程
正文包括:
- 基础泛型
- 索引类型 & 映射类型
- 条件类型 & 分布式条件类型
- infer 关键字
- 类型守卫 is in 关键字
- 内置工具类型机能与原理
- 内置工具类型增强
- 更多通用工具类型
接下来进入正文
泛型 Generic Type
直接上个例子
1 | function foo<T>(arg: T): T { |
很多初学者不理解 T 到底是什么意思,其实它表示的就是一种未知类型,它是入参与返回值的类型。
解释:上面例子表示未,函数 foo 可接受的参数类型为 T , arg 的类型为 T ,函数返回值的类型为 T 。
1 | foo<string>("dylan") |
另外,你可能曾经见过 Array<number>, Map<string, ValueType> 这样的使用方式,通常我们将上面例子中 T 这样的未赋值形式成为 类型参数变量 或者说 泛型类型,而将 Array<number> 这样已经实例化完毕的称为 实际类型参数 或者是 参数化类型。
当然也可以不指定,因为 TS 会自动推导出泛型的实际类型。
1 | ⚠️ 注意 TS 在 泛型在箭头函数下的书写 |
除了在函数中泛型也可以在类中使用:
1 | class Foo<T, U> { |
介绍完泛型的基础概念,接下来讲讲泛型的应用。
类型守卫、is in 关键字
- is
举个栗子
比如现在有个字段
1 | numOrStrProp: number | string; |
使用时,想将这个联合类型缩小范围,比如精确到string,你可能会加个函数去做判断
1 | export const isString = (arg: unknown): boolean => typeof arg === "string"; |
然后使用这个方法
1 | function useIt(numOrStr: number | string) { |
会出现如下错误
1 | 类型“string | number”上不存在属性“length”。 |
说明参数依然是联合类型,这个时候就该使用 is 关键字了:
1 | export const isString = (arg: unknown): arg is string => |
这只是以原始类型为成员的联合类型,再看一个简单的假值判断
1 | export type Falsy = false | "" | 0 | null | undefined; |
类似的,还有 isPrimitive 、isFunction这样的类型守卫。
- in
然后来看下 in 关键字
1 | class A { |
类似 for...in 中的 in 它能判断一个属性是否为一个对象所拥有,通过这种方式可以进行类型收窄。
- 字面量类型(literal types)
然后要说的是字面量类型(literal types),例如:
你的状态码可能是 0/1/2
1 | 你就可以写成 status: 0 | 1 | 2 , 而不是用一个number来表示 |
字面量类型包括 字符串字面量、数字字面量、布尔值字面量
1 | mode: 'dev' | 'prod' |
布尔值字面量通常与其他字面量类型混用.
基于字段区分接口
即登录与未登录下的用户信息是完全不同的接口
1 | interface ILogInUserProps { |
或者通过字面量类型:
1 | interface ICommonUserProps { |
索引类型与映射类型
- 索引类型
先介绍一个语法 索引类型查询 语法 keyof 它会返回后面跟着的类型参数的键值组成的字面量联合类型,举个例子:
1 | interface foo { |
是不是就像 Object.keys() 一样?区别就在于它返回的是联合类型.
接下来,要深刻认识到类型编程也是编程,带着这种思想去阅读接下来的部分。
首先来实现一个简单的函数
1 | // 假设key是obj键名 |
接下来定义类型:
- 参数
obj - 参数
key - 返回值
1 | function pickSingleValue<T>(obj: T, key: keyof T): T[keyof T] { |
解释一下,首先定义 obj 为泛型 T ,key 也就是 obj 键值是 keyof 返回的联合类型,返回值自然就是 T[keyof T]。
这种写法明显又可以改进的地方:
keyof出现了两次T应该被限制为对象类型
1 | function pickSingleValue<T extends object, U extends keyof T>( |
用一个变量把多处出现的存起来,记得,在类型编程里,泛型就是变量。
再看一个概念 索引签名 在类型编程中,索引签名用于快速建立一个内部字段类型相同的接口.
1 | interface Foo { |
那么接口 Foo 实际上等价于一个键值全部为 string 类型,不限制成员的接口。
等同于
Record<string, string>
- 映射类型 Mapped Types
类似于数组 map 方法,在类型编程中我们会从一个类型的定义映射的一个新的类型定义,并在旧的基础上进行一些改造:
- 修改原有接口的键值类型
- 为原有接口的键值类型新增修饰符,例如
readonly、 可选?
从一个简单的场景入手
1 | interface A { |
需求: 实现一个接口跟 A 中的字段完全相同,但类型全部为 string 类型
相当于写一个 utils 里的工具,然后去用就完事儿了
1 | type StringifyA<T> = { |
重要的就是这个 in 操作符, 你可以把它理解为 for...in / for...of 这种遍历思路
获取到键名之后,键值就简单了,再来写一个拷贝新的类型别名
1 | type CloneA<T> = { |
Partial 来个常用的: 将接口下的字段全部变成可选的
1 | type Partial<T> = { |
key?: value 意为这一字段是可选的,在大部分情况下等同于 key: value | undefined
条件类型 Conditional Types
在编程中的条件判断我们常用 if 语句与 三元表达式 实现:
1 | if(condition){ |
而 条件类型 的语法,实际上就是三元表达式,看一个最简单的例子:
1 | T extends U ? X : Y |
理解为 T 继承 U ,也就是说, T 中包含 U 中所有属性
也就是说 T 中必须包含 U 中的所有属性,则是 true ,反之就是 false