likes
comments
collection
share

Typescript温故而知新

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

昨天下午上班摸鱼的时候,突然看到群里有人在学Typescript,问我怎么学。我说多用,然后一想,反正在摸鱼,不如复习一下Typescript(装一下比)。

装逼结果

Typescript温故而知新

首先咱们先来复习一下基础知识,先从泛型讲到工具类型

泛型

type Required<T> = {
  [K in keyof T]: T[K]
}
type Partial<T> = {
  [K in keyof T]?: T[K]
}

以上是泛型的最基本的用法之一,这里的keyof是Typescript的关键字,用于将对象类型的key展开作为联合类型。另外有一个关键字typeof就是用于得到某个对象的类型

我们这里进行一个简单的组合,如果我部分字段想要必填,另外一些字段想要非必填怎么办呢?

type RequiredExcept<T, U extends keyof T> = {
  [K in keyof Pick<T, U>]?: T[K]
} & {
  [K in keyof Omit<T, U>]: T[K]
}

type User = {
  name: string
  age: number
  gender: boolean
}

type Test = RequiredExcept<User, 'name'>

const user: Test = {
  age: 1,
  gender: true
}

工具类型

工具类型指的是那些将泛型作为参数达到某种目的而被广泛使用的类型,比如我们刚刚写的RequiredPartialPickOmit。常见的工具类型比如RecordReturnTypeLowercase等。这里我们介绍几种了解

Record

Record常见于定义键值对的对象的类型。

type Record<K extends string | number | symbol, T> = {
  [P in K]: T
}

Pick和Omit

PickOmit是一对可以起用的,两者刚好相反,前者取得一个类型中的部分字段组合成为一个新类型;后者取得一个类型中的除了所罗列的字段之外的字段组合起来作为新类型

type Pick<T, U extends keyof T> = {
  [K in U]: T[K]
}

type Omit<T, U extends string | number | symbol> = {
  [K in Exclude<keyof T, U>]: T[K]
}

type Exclude<T, U> = T extends U ? never : T

type User = {
  name: string
  age: number
  gender: boolean
}

type NewUser = Pick1<User, 'age' | 'name'>
type NewUser2 = Omit<User, 'age' | 'name'>

可以看到Omit借助了新的工具类型Exclude才实现的,Exclude从定义就可以看出,如果T是U的子类型则返回never(无类型),否则返回T。这就可以将一个类型中在所给定的联合类型里的字段给剔除掉,留下没有在联合类型的字段组合成新的类型

模板字符串类型

没想到吧,模板字符串竟然也能当作类型。普通字符串都能作为类型使用,模板字符串也可以作为类型使用

const user = 'ainuo5213'
type User = typeof user

以上会返回什么类型?我想不熟悉Typescript的大多会说字符串类型。错了,大错特错,字符串类型所表示的字符串是可以更改的,这里的user很明显不可更改,所以他应该是"ainuo5213"类型,这样一个字符串就确定了其不可变。

Typescript温故而知新

那么模板字符串怎么写类型呢?很简单

type Hello<T extends string> = `Hello ${T}`

type HelloWord = Hello<'world'> // Hello world

当然,模板字符串类型也可以和联合类型组合使用,就像下面这样

type MobileBrand = 'iphone' | 'huawei'
type Memory = '16g' | '32g'

type Mobile = `${MobileBrand}-${Memory}`

他会得到如下的组合结果

Typescript温故而知新 我们发现,联合类型分别被组合了,这样我们就得到了一个通用的定义手机的类型

如果我们想定义版本号,比如v1.0.1,也可以用模板字符串进行书写

type AlphaOrBeta = 'alpha' | 'beta'
type Suffix = `-${AlphaOrBeta}.${number}`
type Version = `v${number}.${number}.${number}${Suffix}`

const version: Version = 'v1.0.0-beta.1'

针对字符串类型的工具类型

常用的针对字符串类型的工具类型有CapitalizeUncapitalizeUppercaseLowercase,他们分别是针对字符串的大小写等进行操作,比如我们可以这样使用

type User = {
  name: string
  age: number
}

type ChangeEvent<T> = {
  [K in keyof T as `on${Capitalize<K & string>}Change`]: (newValue: T[K]) => void
}

type UserChangeEvent = ChangeEvent<User>

上面这个例子通过改变映射之后的K的值,和Capitalize进行组合使用就得到了一个ChangeEvent。我们可以查看一下其结果

Typescript温故而知新

infer

infer在大多数的Typescript编程过程中用不到,但是Typescript既然引入了他就必然有其意义

infer可以对类型进行推断,且只能用于类型推断。他可以构建如常用的ReturnTypeParameterType这些对函数类型中返回值和参数的类型推断

type AsyncFunc<T, U> = (...args: T[]) => Promise<U>

type User = {
  name: string
  age: number
}
type GetUserAsyncFunc = AsyncFunc<number, User>

type ParameterType<T> = T extends (...args: [infer R]) => any ? R : never
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

type GetUserAsyncFuncParameterType = ParameterType<GetUserAsyncFunc> // number
type GetUserAsyncFuncReturnType = ReturnType<GetUserAsyncFunc> // Promise<User>

就这样我们可以提取得到一个函数的参数和返回值的类型

infer用法很多样,这里可以去林不渡大佬的小册子进行阅读: [类型里的逻辑运算:条件类型与 infer](juejin.cn/book/708640…)

回归主线,实现ParsedEnvProcessType

了解了infer等相关知识,我们可以着手急性ParsedEnvProcessType的类型编写。

ParsedEnvProcessType是一个将以REACT_APP_开头的环境变量书写成小驼峰形式。了解了之后,开始装逼过程

将前缀去掉

type ProcessEnv = {
  REACT_APP_ENV: string
  REACT_APP_BASE_URL: string
  NODE_ENV: string
  REACT_APP_GRAY_TITLE: boolean
}
type PrunePrefix<T, U extends string> = {
  [K in keyof T as K extends `${U}${infer R}` ? R : K]: T[K]
}
type PrunedPrefixProcessEnv = PrunePrefix<ProcessEnv, 'REACT_APP_'>

看一下结果是否符合预期

Typescript温故而知新 看来还不错,继续

将所有类型字段变成小写

将类型变成小写,就可以结合之前的Lowercase了,全面的知识也串起来了

type ProcessEnv = {
  REACT_APP_ENV: string
  REACT_APP_BASE_URL: string
  NODE_ENV: string
  REACT_APP_GRAY_TITLE: boolean
}
type PrunePrefix<T, U extends string> = {
  [K in keyof T as K extends `${U}${infer R}` ? R : K]: T[K]
}
type PrunedPrefixProcessEnv = PrunePrefix<ProcessEnv, 'REACT_APP_'>
type LowserCased<T> = {
  [K in keyof T as Lowercase<K & string>]: T[K]
}
type LowerCasedPrunedPrefixProcessEnv = LowserCased<PrunedPrefixProcessEnv>

这下看看结果对不对

Typescript温故而知新 还可以,那么咱们 继续

去掉下划线并将下划线首字母大写

去掉下划线的时候,由于不知道字符串有多少个_,所以这里咱们不能很死板的写。

既然我们想要循环去掉_,那能不能用js中的递归呢?答案是肯定的,可以!!!

type Camel<T extends string> = T extends `_${infer R}`
  ? Camel<R>
  : T extends `${infer R}_${infer U}`
  ? `${Uncapitalize<R>}${Capitalize<Camel<U>>}`
  : T

type CameledBaseUrl = Camel<'gray_title_test'>

在这个代码中,我们首先对T是否是_开头做了推断,如果是以_开头的话,就递归之后的R;否则的话,T就不是以_开头,走到了第二个分支。第二个分支对T是否是xx_yy这种类型做了判断,如果是这样的话,我们将R进行小写开头,并将U进行递归,并将最后的值大写开头,这样咱们就得到了一个小驼峰写法的类型了。 可以见一下结果对不对

Typescript温故而知新

将小驼峰和其他代码组合

type ProcessEnv = {
  REACT_APP_ENV: string
  REACT_APP_BASE_URL: string
  NODE_ENV: string
  REACT_APP_GRAY_TITLE: boolean
}
type PrunePrefix<T, U extends string> = {
  [K in keyof T as K extends `${U}${infer R}` ? R : K]: T[K]
}
type PrunedPrefixProcessEnv = PrunePrefix<ProcessEnv, 'REACT_APP_'>
type LowserCased<T> = {
  [K in keyof T as Lowercase<K & string>]: T[K]
}
type LowerCasedPrunedPrefixProcessEnv = LowserCased<PrunedPrefixProcessEnv>
type Camel<T extends string> = T extends `_${infer R}`
  ? Camel<R>
  : T extends `${infer R}_${infer U}`
  ? `${Uncapitalize<R>}${Capitalize<Camel<U>>}`
  : T

type CamelCase<T> = {
  [K in keyof T as Camel<K & string>]: T[K]
}
type ParsedProcessEnvType = CamelCase<LowserCased<PrunePrefix<ProcessEnv, 'REACT_APP_'>>>

CamelCase将类型中的K转成驼峰,然后组合成新的类型。这样咱们就得到了一个小驼峰之后的环境变量类型。

检验一下结果对不对

Typescript温故而知新

发现也是对的,这样咱们的比就装完了。哈哈哈

转载自:https://juejin.cn/post/7218208891707146277
评论
请登录