likes
comments
collection
share

TS类型的进阶使用

作者站长头像
站长
· 阅读数 16

当涉及到 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,但它只能赋值给anyunknown
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 truefalse
}

// 具体实现如下,以检测数组的自定义守卫为例
function isArray (value: any): value is any[] {
    return Array.isArray(value)
}

注:检测数组你可以直接使用Array.isArray,因为它本身就是一个自定义类型守卫(引出它只是为了加深印象),而isPrototypeOf的返回值类型只是一个简单的布尔类型。TS类型的进阶使用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 = []> {
  ......
}

条件类型

  1. 基本使用

条件类型基于条件表达式,根据条件的真假选择不同的类型,类似于 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
  1. 交叉类型取的多个类型的并集,但是如果相同key但是类型不同,则该key为never。
type T13 =  { name: string; }  & { name: symbol; }
const t13: T13 = { name: string } // error: 不能将类型“string”分配给类型“never”。

映射类型

前置知识

  1. typeof 返回一个具体值的类型
const userInfo = {
  name: '钢蛋',
  age: 18,
  gender: '男'
}

type UserInfoType = typeof obj;

// ObjType 等同于下面
type UserInfoType = {
  name: string;
  age: number;
  gender: string;
}
  1. keyof 返回一个对象类型所有的 key , 将其组成一个联合类型
type UserInfoType = {
  name: string;
  age: number;
  gender: string;
}

type UserInfoKeys = keyof UserInfoType

// UserInfoKey 等同于下面
type UserInfoKeys = 'name' | 'age' | 'gender'
  1. ?可选属性 > 如果一个对象类型中的属性为可选, 那么创建对象时,可以选择性地省略该属性。
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: '钢蛋'
}
  1. 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”赋值,因为它是只读属性。

映射类型的简单使用

  1. 简单使用 > 与 JS 中的 for...in 循环和 map 方法相似
type UserInfo = {
  [key in 'name' | 'gender' | 'address']: string
} 

// UserInfo 将转成以下形式
type UserInfo = {
  name: string;
  gender: string;
  address: string;
}
  1. 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;
}
  1. 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` 进行映射
  1. 映射数组 > 映射类型不光可以映射对象, 还可以映射数组
// 将 number[] 转化成 string[]
type ToString<T extends number[]> = {
  [P in keyof T]: `${T[P]}`
}

type NewArr = ToString<[1, 2, 3, 4]> // ["1", "2", "3", "4"]

修饰符

  1. 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];
};
  1. Readonly > 只读 ==> 将对象类型中的所有属性转成只读
// 只读 `readonly` 
type Readonly<T> = {	// 将T中的所有属性设置为只读
    readonly [P in keyof T]: T[P];
};

// 去除只读, 跟去除可选差不多, 如 `-readonly` 

重命名/筛选

as 关键字可以用作类型断言, 将一个类型转化成另一个类型, 但必须满足前面所说的重叠关系, 不可随意转化 在 TypeScript 4.1 及更高版本中,可以使用as映射类型重新映射映射类型中的键

  1. 重命名 > 将对象类型中的每个属性映射成函数并重命名
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;
}
  1. 筛选 > 通过条件类型生成 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…参考资料:

  1. 官网文档:www.typescriptlang.org/zh/
  2. 优质博客:juejin.cn/post/699410…
  3. any与unknown: juejin.cn/post/710849…