TS 中的内置工具

ts 提供了一些全局可用的工具类型来辅助进行常见的类型转换,本文就对它们做个总结与分享,并且会手写实现部分工具类型,或对它们在框架源码中的运用进行举例。

this 相关

先来介绍 3 个与 this 相关的:

ThisParameterType

用于提取函数的类型中 this 的类型。首先通过 typeof 获取函数类型,然后以泛型的写法传给 ThisParameterType 获取 singthis 的类型:

typescript 复制代码
function sing(this: { name: string }, age: number) {
  console.log(`是谁在唱歌? 是${this.name},今年${age}岁`)
}

type singType = typeof sing
type singThisType = ThisParameterType<singType>

获取的 singThisType 结果如下:

至于 sing 函数中第一个参数传 this 这种写法,可参见 《TS 中的函数》

OmitThisParameter

OmitThisParameter 就是用于移除函数类型中的 this 参数类型,然后返回函数的类型:

typescript 复制代码
type singOmitThisType = OmitThisParameter<singType>

sing 的参数去除掉 this 参数,只有一个 age,所以结果如下:

ThisType

ThisType 用于绑定上下文的 this 的类型。在下例中,我们想让 sing 中的 this 指向 singer.info,于是在第 17 行通过 call 去调用,绑定了 this,但 ts 并不知道第 14 行的 this 的类型为 IInfo,所以我们可以通过给 sing 传入 this 方式来进行类型注释:

typescript 复制代码
interface IInfo {
  name: string
}
interface ISinger {
  info: IInfo
  sing: () => void
}

const singer: ISinger = {
  info: {
    name: 'Jay'
  },
  sing(this: IInfo) {
    console.log(`是谁在唱歌? 是${this.name}`)
  }
}
singer.sing.call(singer.info)

但如果 singer 中有好几个方法都需要让 this 指向 info 呢?每个方法都传一次 this 显得麻烦。还有一种办法,就是使用 ThisType,告诉 ts 在 singer 中,this 的类型为 IInfo

typescript 复制代码
// ...
const singer: ISinger & ThisType<IInfo> = {
  info: {
    name: 'Jay'
  },
  sing() {
    console.log(`是谁在唱歌? 是${this.name}`)
  }
}

注意:这里使用了交叉类型 & 来表明 singer 的类型是ISinger 的同时,this 的类型为 IInfo

映射类型相关

先来介绍下何谓映射类型

映射类型(Mapped Types)

假设现有 IPerson

typescript 复制代码
interface IPerson {
  name: string
  age: number
}

其内的属性都是必须的,如果我们想定义一个同样有着 nameage 属性的类型,只不过属性都是可选的,就可以通过映射类型实现,其写法融合了泛型索引签名的知识点:

typescript 复制代码
type MapPartialType<T> = {
  [p in keyof T]?: T[p]
}

type PersonPartialType = MapPartialType<IPerson>

keyof T 得到的是 T 的各个属性的键的联合类型,keyof IPerson 得到的相当于是 'name' | 'age'p in keyof T 也就是将联合类型中的每个类型(T 类型中的所有属性)都遍历一遍,然后各自赋值为 T[p],不加 ? 就是完全复制了一个 T 类型。

现在查看 PersonPartialType 的类型,结果如下:

如果原本 IPerson 的属性就是可选的,可以使用 -? 将新类型改为属性都是必须的:

除了可以添加 ?-?,也可以在 [p in keyof T] 前面添加 readonly 让新类型的属性都是只读的:

Partial、Required 与 Readonly

上面使用联合类型实现的 MapPartialTypeMapRequiredTypeMapReadonlyType 其实就是 ts 提供的内置工具 PartialRequiredReadonly 的实现:

  • Partial<T> 传入一个泛型,将 T 的所有属性变为可选的;
  • Required<T> 传入一个泛型,将 T 的所有属性变为必须的;
  • Readonly<T> 传入一个泛型,将 T 的所有属性变为只读的。
typescript 复制代码
type PersonPartialType = Partial<IPerson>
type PersonRequiredType = Required<IPerson>
type PersonReadonlyType = Readonly<IPerson>

Record

Record<K, T> 传入 2 个泛型,得到的对象类型中,属性(键)都是 K (联合类型)里的类型,值都是 T 类型:

typescript 复制代码
type t1 = 'a' | 'b'
interface IPerson {
  name: string
  age: number
}

type RecordType = Record<t1, IPerson>

手写实现如下,为了保证传入的值是可以作为键的类型,所以 K 需要 extends keyof any,效果等同于 extends string | number | symbol

typescript 复制代码
type MyRecord<K extends keyof any, T> = {
  [p in K]: T
}

比如在 vue router 中,RouteRecordNormalized 接口的属性 components,其类型就用到了 Record

Pick

从一个类型中挑选几个属性组成一个新类型:

typescript 复制代码
interface IPerson {
  name: string
  age: number
  gender: string
}


type PickType = Pick<IPerson, 'name' | 'age'>

手写实现比较简单,只要保证传入的 K 的类型是 T 的属性即可:

typescript 复制代码
type MyPick<T, K extends keyof T> = {
  [p in K]: T[p]
}

在一些框架的源码里,Pick 被使用的频率还是挺高的,比如 nuxt3 中对于接口 HeadSafe 的定义:

Omit

从一个类型中去掉几个属性,剩余的属性组成一个新类型:

typescript 复制代码
interface IPerson {
  name: string
  age: number
  gender: string
}

type OmitType = Omit<IPerson, 'gender'>

手写实现如下,先让 p in keyof T 获取 T 的每个属性,然后 as p 获取到新的 p,对这个 p 做个条件判断,看看是否继承自 K,如果是就去掉,返回 never,如果不是则保留,返回 p 本身:

typescript 复制代码
type MyOmit<T, K extends keyof T> = {
  [p in keyof T as p extends K ? never : p]: T[p]
}

在 nuxt3 的源码中,类型别名 HeadPluginOptions 的定义中,就用到了 Omit:

分发条件类型相关

所谓分发条件类型(Distributive Conditional Types),在使用泛型时,如果传入了一个联合类型,运用条件类型对泛型做判断时,联合类型的每个类型都会被测试是否满足条件。

Extract

Extract<T, U> 用于从一个联合类型中挑选几个类型组成新类型:

typescript 复制代码
type gender = 'male' | 'female' | 'others'
type ExtractType = Extract<gender, 'male' | 'female'>

其实现如下,泛型 T 传入了一个联合类型,T extends U ? T : never 就会把 T 的每个类型('male''female''others')单独拿出来检测下是不是继承自 U,是则保留,返回 T ,不是就排除,返回 never

typescript 复制代码
type MyExtract<T, U> = T extends U ? T : never

Exclude

Extract 功能相反,Exclude<T, U> 用来对联合类型的成员进行排除:

typescript 复制代码
type gender = 'male' | 'female' | 'others'
type ExcludeType = Exclude<gender, 'others'>

其实现只需将之前实现的 MyExtract 的判断返回结果的去留做个顺序对调即可:

typescript 复制代码
type MyExclude<T, U> = T extends U ? never : T

NonNullable

NonNullable<T> 用于去除联合类型中的 nullundefined

typescript 复制代码
type gender = 'male' | 'female' | null | undefined
type NonNullableType = NonNullable<gender>

实现如下:

typescript 复制代码
type MyNonNullable<T> = T extends null | undefined ? never : T

相关推荐
web13508588635几秒前
前端node.js
前端·node.js·vim
m0_512744641 分钟前
极客大挑战2024-web-wp(详细)
android·前端
潜意识起点25 分钟前
精通 CSS 阴影效果:从基础到高级应用
前端·css
奋斗吧程序媛30 分钟前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode
IT女孩儿39 分钟前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
m0_748256563 小时前
如何解决前端发送数据到后端为空的问题
前端
请叫我飞哥@3 小时前
HTML5适配手机
前端·html·html5
@解忧杂货铺5 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
F-2H6 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
gqkmiss7 小时前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件