TypeScript类型体操(一)

本文章是前一章ts基础的加深版,意在深入练习下ts,主要是练习标识符infer
先来个大总结:

  • 关键字infer 不仅可以推导返回值/ 参数。其实还可以推导指定位置的值,例如:字符串/ 数组等
  • 如果在使用中用到了长度,需要使用元组的特性length来解决问题

1、CapitalizeString

将字符串的首字母进行大写转换,实例如下:

handler => Handler
parent => Parent
233 => 233
type Index<T> = T extends `${infer L}${infer R}` ? `${Uppercase<L>}${R}` : never
// 首字母大写

type a1 = Index<'handler'>       // Handler
type a2 = Index<'parent'>        // Parent
type a3 = Index<233>             // 233

export {}
  • 上述代码中标识符infer用来进行推断的,不仅可以推断返回值/ 参数/ 构造函数等,同样可以推断字符串
  • 代码${infer L}${infer R} 表示将字符串推断两部分h 以及andler。也就是infer L 推断第一个字母,而infer R用来推断其余的字符
  • 代码Uppercase用来转换大写

2、FirstChar

获取字符串的第一个字符, 意思其实很简单。这里就不过多举例了,直接上代码

type FirstChar<T> = T extends `${infer L}${infer R}` ? L : never
// 获取字符串字面量中的第一个字符

type A = FirstChar<'BFE'> // 'B'
type B = FirstChar<'dev'> // 'd'
type C = FirstChar<''> // never

export default {}

3、LastChar

获取最后一个字符串,其实题2有点类似,但是相对来说比较复杂,这里会做详细描述

// 获取字符串字面量中的最后一个字符
type LastChar<T, Prev = never> = T extends `${infer L}${infer R}` ? LastChar<R, L> : Prev

type A = LastChar<'BFE'> // 'E'
type B = LastChar<'dev'> // 'v'
type C = LastChar<''> // never

export default {}
  • 这到底可以看出来,有时候我们需要更多的参数来进行辅助,比如实例中的Prev
  • 还是使用infer L 以及infert R 来辅助分解字符串
    • 首先以字符串BFE为实例
    • 第一次执行:L 是B R是FE,递归调用LastChar的时候传值是:LastChar<FE, B>
    • 第二次执行:L 是F R是E,递归调用LastChar的时候传值是:LastChar<F, E>
    • 依次类推

4、StringToTuple

字符串转换为元组类型

// 字符串转换为元组类型
type StringToTuple<T, S extends any[] = []> = T extends `${infer L}${infer R}` ? StringToTuple<R, [...S, L]> : S

type A = StringToTuple<'BFE.dev'> // ['B', 'F', 'E', '.', 'd', 'e','v'] 
type B = StringToTuple<''> // []

export default {}
  • 核心原则就是多定义一个参数数组,每次递归的时候用数组来接受参数,最后数组就是返回值

5、TupleToString

将字符串类型的元素转换为字符串字面量类型

// 将字符串类型的元素转换为字符串字面量类型

type TupleToString<T, S extends  string = ''> = T extends [infer L, ...infer R] ? (
  L extends string ? TupleToString<R, `${S}${L}`> : never
) : S

type A = TupleToString<['a', 'b', 'c']> // 'abc'
type B = TupleToString<[]>              // ''
type C = TupleToString<['a']>           // 'a'

export default {}
  • 其实关键字infer不仅可以对字符串进行推导,对数组同样可以进行推导。而且数组的推导比字符串更加灵活,下面以数组const arr = [a, b, c]进行举例
    • [infer L, ...infer R] => L 是a R是[b, c ]

6、RepeatString

复制字符T为字符串类型,长度为C

// 复制字符T为字符串类型,长度为C

type RepeatString<T extends string, K, A extends any[]= [], S extends string = ''> = K extends A["length"] ? S : RepeatString<T, K, [...A, T], `${S}${T}`>

type A = RepeatString<'a', 3> // 'aaa'
type B = RepeatString<'a', 0> // ''

export default {}
  • 通过上述的实例:如果使用长度的话就要用到元组,因为元组的length属性可以判断长度
  • 上述的实例比较简单,这里就不会做过多的阐述了

7、SplitString

将字符串字面量类型按照指定字符,分割为元组。无法分割则返回原字符串字面量

// 将字符串字面量类型按照指定字符,分割为元组。无法分割则返回原字符串字面量
type SplitString<T, K extends string = '', S extends any[] = [T]> = T extends `${infer L}${K}${infer R}` ? SplitString<R, K, [...S, L]> : S

type A1 = SplitString<'handle-open-flag', '-'>        // ["handle", "open", "flag"]
type A2 = SplitString<'open-flag', '-'>               // ["open", "flag"]
type A3 = SplitString<'handle.open.flag', '.'>        // ["handle", "open", "flag"]
type A4 = SplitString<'open.flag', '.'>               // ["open", "flag"]
type A5 = SplitString<'open.flag', '-'>               // ["open.flag"]

export default {}
  • 上述问题的解决关键在于数组类型的推导
  • 代码${infer L}${K}${infer R} 最为重要。以字符串open-flag为例,如果K是-的话,那推导出来的就是L => open. R => flag等

8、LengthOfString

计算字符串字面量类型的长度

// 计算字符串字面量类型的长度
type LengthOfString<T, S extends any[] = []> = T extends `${infer L}${infer R}` ? LengthOfString<R, [...S, L]> : S["length"]

type A = LengthOfString<'BFE.dev'> // 7
type B = LengthOfString<''> // 0

export default {}
  • 解决问题的关键在于:如果遇到了计算长度的问题,就必须利用元组的特性来解决

9、KebabCase

驼峰命名转横杠命名

// 驼峰命名转横杠命名

type KebabCase<T, S extends string = ''> = T extends `${infer L}${infer R}` ? (
  Uppercase<L> extends L ? KebabCase<R, `${S extends '' ? S : `${S}-`}${Lowercase<L>}`> : KebabCase<R, `${S}${L}`>
) : S

type a1 = KebabCase<'HandleOpenFlag'>           // handle-open-flag
type a2 = KebabCase<'OpenFlag'>                 // open-flag

export default {}
  • 解决这个问题的关键在于:如果识别大写字母,识别后把大写字母 转换为 小写字母+ -来解决
  • 我们可以通过方法Uppercase<L> extends L 来识别大写字母

10、CamelCase

横杠命名转化为驼峰命名

// 横杠命名转化为驼峰命名

type FirstUppercase<T>  = T extends `${infer L}${infer R}` ? `${Uppercase<L>}${R}` : T
type CamelCase<T extends string = '', S extends string = ''> = T extends `${infer L}-${infer R}` ? CamelCase<R, `${FirstUppercase<S>}${FirstUppercase<L>}`> : `${S}${FirstUppercase<T>}`
type a1 = CamelCase<'handle-open-flag'>         // HandleOpenFlag
type a2 = CamelCase<'open-flag'>                // OpenFlag

export default CamelCase
  • 提取每个符号-左右的字符,将提取的字符首字母大写,然后拼接到一起就行了

11、ObjectAccessPaths

进行对象属性拼接,而且可以进行类型识别

const i18n = createI18n({
  home: {
    topBar: {
      title: '顶部标题',
      welcome: '欢迎登录'
    },
    bottomBar: {
      notes: 'XXX备案,归XXX所有',
    },
  },
  login: {
    username: '用户名',
    password: '密码'
  }
})

// 上述对象转换出来的类型是:
// home.topBar.title | home.topBar.welcome | home.bottomBar.notes | login.username | login.password

i18n('home.topBar.title')           // √
i18n('home.topBar.welcome')         // √
i18n('home.bottomBar.notes')        // √
i18n('home.bottomBar.notes1')        // ×
type trimFirstCode<T> = T extends `.${infer R}` ? R : T

type ObjectAccessPaths<T, R extends string = '', K = keyof T> = K extends keyof T ? (
  K extends string ? (
    T[K] extends Record<string, any> ? ObjectAccessPaths<T[K], trimFirstCode<`${R}.${K}`>> : trimFirstCode<`${R}.${K}`>
  ) : never
) : never

// 完成 createI18n 函数中的 ObjectAccessPaths<Schema>,限制函数i18n的参数为合法的属性访问字符串
function createI18n<Schema>(schema: Schema): ((path: ObjectAccessPaths<Schema>) => string) {return [{schema}] as any}

// i18n函数的参数类型为:home.topBar.title | home.topBar.welcome | home.bottomBar.notes | login.username | login.password
const i18n = createI18n({
  home: {
    topBar: {
      title: '顶部标题',
      welcome: '欢迎登录'
    },
    bottomBar: {
      notes: 'XXX备案,归XXX所有',
    },
  },
  login: {
    username: '用户名',
    password: '密码'
  }
})

i18n('home.topBar.title')           // correct
i18n('home.topBar.welcome')         // correct
i18n('home.bottomBar.notes')        // correct

export default {}
  • 其实只要理解后问题就很简单,最根本的目的就是如何将每个属性都拼接为字符串
  • 上述代码中出现了K extends keyof T. 此时的K一定是联合类型。我们可以利用联合的分发机制来做这个事情

12、ComponentEmitsType

组件的key值以及函数变化
例如:('Handle-open': (flag: boolean) => true) => onHandleOpen: (flag: boolean) => void

import CamelCase from '../10、CamelCase/index';

type Source = {
  'Handle-open': (flag: boolean) => true,
  'preview-Item': (data: {item: any, index: number}) => true,
  'close-item': (data: {item: any, index: number}) => true
}

type ComponentEmitsType<T> = {
  [P in keyof T as `on${P extends string ? CamelCase<P> : ''}`]?: T[P] extends ((...args: infer R) => any) ? (...args: R) => void : T[P]
}

type Result = ComponentEmitsType<Source>

export default {}
  • 我觉得这个也是特别简单的。如果能理解两个点就够了
    • 通过关键字in 进行循环的时候,可以使用as 来重新定义对象的key。如上述代码[P in keyof T as on${P extends string ? CamelCase<P> : ''}]
    • 还有就是通过关键字infer 来推导参数

end

今天关于TS进阶体操就分享这么多,以上方法都是作者本人进行解答,如果读者有什么别的方案,可以通过评论告诉我。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
TypeScript具有丰富的类型系统,可以帮助开发者在编码过程中捕获错误、提供代码补全和自动提示等功能。以下是一些常见的 TypeScript 类型: 1. 原始类型: - boolean: 布尔类型,只能是 true 或 false。 - number: 数字类型,包括整数和浮点数。 - string: 字符串类型。 - null 和 undefined: 分别表示空值和未定义值。 2. 数组类型: - 基本数组类型:例如 number[] 表示数字数组,string[] 表示字符串数组。 - 泛型数组类型:使用泛型语法定义特定类型的数组,例如 Array<number> 表示数字数组。 3. 元组类型: - 元组是固定长度和特定类型的数组。 - 例如 [string, number] 表示包含一个字符串和一个数字的元组。 4. 对象类型: - 对象类型可以使用字面量语法定义,例如 { name: string, age: number } 表示一个具有 name 和 age 属性的对象。 - 也可以使用接口(interface)或类(class)定义对象类型。 5. 函数类型: - 函数类型包括参数的类型和返回值的类型,例如 (a: number, b: number) => number 表示接收两个数字参数并返回一个数字的函数类型。 6. 枚举类型: - 枚举用于定义一组命名的常量值。 - 例如 enum Color { Red, Green, Blue } 定义了一个颜色的枚举类型,可以通过 Color.Red、Color.Green 等访问枚举值。 除了以上类型TypeScript 还支持联合类型、交叉类型类型别名、类型推断和泛型等高级特性,可以更灵活地应对各种编程场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值