TS类型的进阶使用
当涉及到 TypeScript 时,类型是一个非常重要的概念。在 TypeScript 中,类型可以帮助我们在编写代码时捕获错误,提高代码的可读性和可维护性。 假设你对 TypeScript 已经有了最基本的了解,这里我们将探讨 TypeScript 类型的进阶使用,帮助您成为一名“体操运动员”。我们将涵盖泛型、条件类型、映射类型、类型推导等概念,以及如何使用它们来编写更健壮、更可维护的代码。
interface 与 type
- type:类型别名,可定义任何类型,此处省略500字(暂时跳过)
- interface:仅可定义对象、函数、数组
// 1.定义对象
interface BaseResponse {
code: number;
message: string;
}
// 2.定义函数 > 并为函数添加属性
interface Func {
_name: string // 函数的属性
getName: () => string // 函数的方法
(...args: any[]): string // 函数类型
}
// 3.定义数组
interface NumKeyObj {
// 【数字索引签名】因为数组也可以看作是一种特殊的对象,它的索引是数字类型
[key: number]: any
}
// NumKeyObj 类型还可以接受数字索引的【对象】
const obj: NumKeyObj = {}
obj['0'] = 1 // √
obj['name'] = 2 // × 索引表达式的类型不为 "number"
补充:在 js 中对象的 key 为数字时,js 引擎会将其转化成字符串,所以 obj[0]
和obj['0']
是等价的
- interface 与 type 的其它不同之处1. interface 可以
extends
继承另一个 interface 或 type 声明的类型,而 type 没有继承2. 相同名字的 interface 会进行合并,而 type 不能存在相同名字,但 type 进行合并使用&
,类型1 & 类型2
这种形式被称为交叉类型
any / unknown / 类型守卫
任何类型都可以被归为 any 和 unknown 类型,两者都可以是其它类型的父类
any
可以理解为我不在乎它的类型,任何类型可以赋值给any
,同时any
类型的值也可以复制给任何类型unknown
可以理解为我不知道它的类型,任何类型可以赋值给unknown
,但它只能赋值给any
和unknown
const a: any = "字符串A";
const b: number = a; // √
const a: unknown = "字符串A";
const b: string = a; // error:不能将类型“unknown”分配给类型“string”。
**警告:**使用
any
类型会使代码失去类型检查的优势,因为any
类型可以接受任何类型的值,而不会进行类型检查。这意味着如果我们在代码中使用any
类型,就无法保证代码的类型安全性,可能会导致一些潜在的类型错误。合理使用any
不要滥用
- 类型守卫(Type Guard):块级作用域中缩小变量类型的一种类型推断行为——缩小类型的范围
- 常见的类型守卫:typeof、in、instanceof、**运算符(===、!==、&&)、自定义类型守卫
function test(args: unknown) {
// 使用 typeof 缩小类型范围,实则就是一个条件判断
if (typeof args === "string") {
return args.split("");
}
}
注意:不是所有的条件判断都会有效,如下
interface A {
name: string;
age: number;
}
// 参数 args 是一个A类型的对象,或者是元素为A类型的数组
function test3(args: A | A[]) {
// 判断 args 是否为数组
if (Array.prototype.isPrototypeOf(args)) {
return args.map(item => item.name); // error:类型“A | A[]”上不存在属性“map”。类型“A”上不存在属性“map”。
}
}
- 自定义类型守卫:当现有的类型守卫无法满足需求时即可自定义
// 语法
function 函数名(形参: 参数类型【参数类型大多为any】): 形参 is 类型 {
return true 或 false
}
// 具体实现如下,以检测数组的自定义守卫为例
function isArray (value: any): value is any[] {
return Array.isArray(value)
}
注:检测数组你可以直接使用Array.isArray
,因为它本身就是一个自定义类型守卫(引出它只是为了加深印象),而isPrototypeOf
的返回值类型只是一个简单的布尔类型。Vue3中的
isRef
也同样是一个自定义类型守卫
export declare function isRef<T>(r: Ref<T> | unknown): r is Ref<T>;
泛型
思想:参数化类型,它允许我们编写可以适用于多种类型的代码,而不必针对每种类型都编写不同的代码。泛型可以在编译时检查类型,从而提高代码的类型安全性和可重用性。
- 简单使用,泛型 T 可以用任意英文字符代替,常用 A ~ Z 表示
interface Response<T> {
code: number;
message: string;
data: T;
}
// 具体使用:Response<string>
- 泛型函数
// 声明式
function identity<T>(arg: T): T {
return arg;
}
// 表达式
const identity = <T>(arg: T): T => {
return arg;
}
const result = identity([1, 2, 3]); // 自动推导类型为 number[]
// const result1 = identity<number[]>([1, 2, 3]); // 传入类型参数
// ^^^^^^^ 一般无需传入类型参数,而是利用类型推论
- 泛型约束 / 默认值
// 泛型约束
interface Response<T extends unknown[]> {
code: number;
message: string;
data: T;
}
// 默认值
interface Response<T, V = []> {
......
}
条件类型
- 基本使用
条件类型基于条件表达式,根据条件的真假选择不同的类型,类似于 js 中的条件表达式
// 当左边的类型可以分配给右边的类型时,则返回第一个
type IsString<T> = T extends string ? string : number;
type A = IsString<string>; // string
协变与逆变
- 协变:如果 A 可以赋值给 B,那么
A extends B
为协变——子类可以赋值给父类
type IfExtends1 = "" extends string ? true : false; // true
type IfExtends2 = string extends "" ? true : false; // false
- 逆变:如果 A 可以赋值给 B,那么
((x: B) => void) extends ((x: A) => void)
为逆变——父类可以赋值给子类
type IfExtends3 = ((x: "") => void) extends ((x: string) => void) ? true : false; // false
type IfExtends4 = ((x: string) => void) extends ((x: "") => void) ? true : false; // true
- 重叠关系
// 继承关系
interface A {
name: string;
}
interface B extends A{
age: number;
}
type IfExtends5 = B extends A ? true : false; // true
// 重叠关系
interface A {
name: string;
}
interface B {
name: string;
age: number;
}
type IfExtends5 = B extends A ? true : false; // true
补充:其实继承关系也存在重叠关系,因为 B 继承于 A,那么 B 将拥有 A 的所有属性和方法,类的继承也同样如此
infer
类型推导,在条件类型中推断类型变量,必须结合 extends 一起使用
- 简单使用 > 获取数组中元素的类型
type ItemType<T> = T extends Array<infer U> ? U : T
^^^^^^^^^
type T1 = ItemType<number[]> // number
解:可以将infer U
理解成一个占位符,Array<不知道是什么类型> extends Array<???>
,等左边的类型确定了,我再自动推导出它的类型
- 实际运用 > ReturnType > 获取函数的返回值类型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
^^^^^^^ ^^^^^^^^^
function getName() {
return "钢蛋"
}
type NameType = ReturnType<typeof getName> // string
// 1. `T extends (...args: any) => any` 约束泛型 T 为一个函数类型
// 2. `(...args: any) => string` 比较 `(...args: any) => infer R` >>> 左边的函数返回值为 sting, 那么 infer 会推导出 R 为 string
补充:函数有一些特殊的返回类型,void 和 nevervoid
类型表示函数没有返回值或没有明确的返回类型。当一个函数没有指定返回值时,默认的返回类型是 void。never
类型表示那些永远不会发生的值的类型。它通常用于表示永远不会返回结果的函数, 如抛出异常的函数或无限循环。
- ⚠ 递归 > Awaited > 模拟 await, 返回 Promise 对象的值 (fulfilled状态下)
type Awaited<T> =
T extends null | undefined ? T : // 1. 当不在严格模式时, null | undefined 可以是任意类型的子类, 所以进行特殊处理
T extends object & { then(onfulfilled: infer F, ...args: infer _): any } ? // 2. 通过是否具有 then 方法去判断是否为一个Promise
F extends ((value: infer V, ...args: infer _) => any) ? // 3. 如果 then 的参数 onfulfilled 是一个函数则提取第一个参数
Awaited<V> : // 4. 因为 onfulfilled 返回的可能还是一个Promise, 所以采用递归展开值
never : // 5. then 的参数 onfulfilled 不是一个函数, 用 never 处理
T; // 6. 不是一个 Promise 则直接返回自身
Promise的更多细节点在这里: Promises/A+
- 模板字符串类型
模板字符串, 与JS的模板字符串语法一样, 但TS的模板字符串是用于类型.
- 简单使用, 将类型作为变量插入到指定位置
type World = "world";
type Greeting = `hello ${World}`; // "hello world"
- ⚠ 递归小技巧 -- 递归尾调:在递归调用时,将当前的计算结果作为参数传递给下一次递归调用
返回字符串的长度
type LengthOfString<S extends string, T extends unknown[] = []> = S extends `${infer First}${infer Rest}`
? LengthOfString<Rest, [...T, First]>
: T['length']
type StrLength = LengthOfString<'我就是个打酱油的'> // 8
解: 1. 先将字符串转成数组, 再使用索引访问类型 T['length']
拿到字符串的长度2. 如果不用递归尾调的形式, 那我们可能会写两个类型, 一个是将字符串转成数组的类型,一个是使用索引访问类型拿到数组的长度3. 疑问: 为什么字符串上也有 length
为什么不直接使用? 答: 因为字符串的索引访问无法拿到具体的数字, 只能拿到一个 number 类型
看到这你可能会吐槽, 模板字符串类型到底有啥用啊, 我肯定是吃饱了撑着才会写这个. 但在某些处理字符串的场景下, 没它还真不行, 感兴趣的可以去研究一下 loadsh 中 get 函数的类型.
联合类型
联合类型表示一个值可以是几种类型之一, 使用
|
运算符可以将多个类型组合成联合类型。当我们不确定一个值的具体类型时,我们可以使用联合类型来增强灵活性。
- 基本使用 > 在进行成员访问的时候,只能访问到其交集部分(多个类型共有的属性/方法)
// 图形名字
enum Diagram {
SQUARE = "圆形",
ROUND = "正方形"
}
interface Square {
name: Diagram.SQUARE,
side: number // 边长
}
interface Round {
name: Diagram.ROUND,
radius: number // 半径
}
function getArea(shape: Square | Round) {
// if (shape.side) >>> error: 类型“Square | Round”上不存在属性“side”。类型“Round”上不存在属性“side”。
if (shape.name === Diagram.SQUARE) { // 类型保护, 缩小类型的范围
return shape.side * shape.side
} else {
return Math.PI * shape.radius ** 2
}
}
- 联合类型的泛型分布式
根据联合类型中的每个成员进行分布式处理,从而推断出联合类型的组合结果。
type T1 = string | number | symbol extends string ? true : never // never
type T2<T> = T extends string ? true : never
type T3 = T2<string | number | symbol> // true
// 非泛型: T1 可以理解为是直接拿 string | number | symbol 和 string 进行比较
// 泛型: T2 可以理解为它是将联合类型`拆分`为单个, 依次和 string 进行比较
- Extract / Exclude
// Extract: 从T中提取可赋值给U的类型
type Extract<T, U> = T extends U ? T : never;
type Test = Extract<string | number | symbol, string>; // string
// Exclude: 从T中排除那些可赋值给U的类型
type Exclude<T, U> = T extends U ? never : T;
type Test = Exclude<string | number | symbol, string>; // number | symbol
- 阻止泛型分布式 > 使用
[]
括起来 > 转化成元组的形式进行比较
type T4<T> = [T] extends [string] ? true : never
type T5 = T4<string | number | symbol> // never
补充: 元组相对于数组的区别是, 元组的每个位置的类型都是固定的, 如 [string, string, number]
, 它是不可变的, 每个位置只能接受对应的类型
- 特定情况逆变为交叉类型
type T8<T, U> = ((a: T) => any) | ((b: U) => any) extends ((ab: infer R) => any) ? R : never
type T9 = T8<{ name: string }, { age: number }> // { name: string; } & { age: number; }
- 交叉类型注意点 a. 基本数据类型之间无法现实交叉, 交叉的结果将是
never
, 如string & number
或"1" & 2
type T11 = string & number // never
type T12 = '1' & 2 // never
- 交叉类型取的多个类型的并集,但是如果相同key但是类型不同,则该key为never。
type T13 = { name: string; } & { name: symbol; }
const t13: T13 = { name: string } // error: 不能将类型“string”分配给类型“never”。
映射类型
前置知识
typeof
返回一个具体值的类型
const userInfo = {
name: '钢蛋',
age: 18,
gender: '男'
}
type UserInfoType = typeof obj;
// ObjType 等同于下面
type UserInfoType = {
name: string;
age: number;
gender: string;
}
keyof
返回一个对象类型所有的 key , 将其组成一个联合类型
type UserInfoType = {
name: string;
age: number;
gender: string;
}
type UserInfoKeys = keyof UserInfoType
// UserInfoKey 等同于下面
type UserInfoKeys = 'name' | 'age' | 'gender'
?
可选属性 > 如果一个对象类型中的属性为可选, 那么创建对象时,可以选择性地省略该属性。
interface Person {
id: symbol;
name: string;
age?: number;
phone?: string;
address?: string;
}
const person: Person = { // error: 类型 "{ id: symbol; }" 中缺少属性 "name",但类型 "Person" 中需要该属性。
id: Symbol(),
}
const person: Person = { // √ 因为其它属性都是可选的, 所以可以省略
id: Symbol(),
name: '钢蛋'
}
readonly
只读属性 > 如果一个对象类型中的属性为只读, 那么就不可以被更改
interface Person {
readonly id: symbol;
readonly name: string;
age?: number;
phone?: string;
address?: string;
}
const person: Person = {
id: Symbol(),
name: '钢蛋',
}
person.name = '铁锤' // error: 无法为“name”赋值,因为它是只读属性。
映射类型的简单使用
- 简单使用 > 与 JS 中的
for...in
循环和map
方法相似
type UserInfo = {
[key in 'name' | 'gender' | 'address']: string
}
// UserInfo 将转成以下形式
type UserInfo = {
name: string;
gender: string;
address: string;
}
Pick
> 提取数据类型的指定属性
type Pick<T, K extends keyof T> = {
[P in K]: T[P]; // T[P] 索引访问,返回对应键值的类型
};
type NewUserInfo = Pick<UserInfo, 'name' | 'address'>
// NewUserInfo 将转成以下形式, 只有 'name' 和 'address', 而没有了 'gender'
type NewUserInfo = {
name: string;
address: string;
}
Omit
> 与Pick
相反, 去除数据类型的指定属性
type Exclude<T, U> = T extends U ? never : T;
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 解析
// 1. `Exclude<keyof T, K>` 先去除指定的key
// 2. 再使用 `Pick` 进行映射
- 映射数组 > 映射类型不光可以映射对象, 还可以映射数组
// 将 number[] 转化成 string[]
type ToString<T extends number[]> = {
[P in keyof T]: `${T[P]}`
}
type NewArr = ToString<[1, 2, 3, 4]> // ["1", "2", "3", "4"]
修饰符
- Partial > 可选 / Required > 必需(去除可选) ==> 将对象类型中的所有属性转成可选/必需
interface Person {
id: symbol;
name: string;
age: number;
phone: string;
address: string;
}
// 可选 `?`
type Partial<T> = { // 使T中的所有属性都可选
[P in keyof T]?: T[P];
};
// 去除可选 `-?`
type Required<T> = { // 使T中的所有属性都必需
[P in keyof T]-?: T[P];
};
- Readonly > 只读 ==> 将对象类型中的所有属性转成只读
// 只读 `readonly`
type Readonly<T> = { // 将T中的所有属性设置为只读
readonly [P in keyof T]: T[P];
};
// 去除只读, 跟去除可选差不多, 如 `-readonly`
重命名/筛选
as
关键字可以用作类型断言, 将一个类型转化成另一个类型, 但必须满足前面所说的重叠关系, 不可随意转化 在 TypeScript 4.1 及更高版本中,可以使用as映射类型重新映射映射类型中的键
- 重命名 > 将对象类型中的每个属性映射成函数并重命名
interface Person {
id: symbol;
name: string;
age: number;
phone: string;
address: string;
}
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]
};
type LazyPerson = Getters<Person>;
// LazyPerson 将转化成以下形式
type LazyPerson = {
getId: () => symbol;
getName: () => string;
getAge: () => number;
getPhone: () => string;
getAddress: () => string;
}
- 筛选 > 通过条件类型生成
never
来筛选出键 > 实现Omit
type Omit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P]
};
type NewPerson = Omit<Person, "address" | "phone" | "age">;
// NewPerson 会转换成以下形式
type NewPerson = {
id: symbol;
name: string;
}
写在最后
类型挑战(强烈推荐,一条通往山顶的路):github.com/type-challe…参考资料:
- 官网文档:www.typescriptlang.org/zh/
- 优质博客:juejin.cn/post/699410…
- any与unknown: juejin.cn/post/710849…
转载自:https://juejin.cn/post/7248544228291772471