Typescript学习(十四)模版字符串类型

102 阅读2分钟

基本使用

在Typescript中, 有很多和Javascript相似的概念, 例如: 泛型可以看作是类型函数的参数; 而本节要介绍的则是另一个相似的点, 模版字符串类型; 所谓的模版字符串类型, 其实语法上和Javascript的模版字符串相同, 唯一的区别就是, 它表示一个类型, 而Javascript中的模版字符串, 是一个值

type name = 'jack'
type str = `hello ${name}`

let wish:str = 'hello jack'

可以看到, 模版字符串的产物, 其实就是字符串字面量类型; 而我们的${}这个插槽(姑且称其为插槽)中, 还可以放入原始类型

type str = `hello ${string}`

let wish1:str = 'hello jack1'
let wish2:str = 'hello world'
let wish3:str = 'hello today'
let wish4:str = 'hallo world' // 报错

当传入的不是一个变量, 而是一个原始类型的时候, 其实就是允许这个插槽内放入这个类型的任意子类型, 从某种意义上来讲, 模版字符串使字符串字面量类型变得更加灵活了; 之前说类型层级的时候, 我们学习过, 字符串字面量类型再往上走, 就是联合类型和原始类型; 今天, 学习了字符串模版类型, 我们可以说, 在字符串字面量类型之上, 还有一个模版字符串类型;

// 所有hello 开头的字符串字面量类型都是str的子类型
type str = `hello ${string}`
type bool = 'hello world' extends str ? true : false // true

// 也可以创建一个表示版本的类型
type versionType = `${number}.${number}.${number}`
let version:versionType = '1.0.0'

但是同时也要注意了, 模版字符串的插槽, 不是来者不拒的, 它能接受的类型只有string | number | bigint | boolean | null | undefined, 除此之外其他类型是不能接受的

type str = `hello ${object}` // 报错

如果, 你给插槽赋值一个never类型, 那么整个模版字符串类型都会变成never

type str = `hello ${never}` // never

自动分发特性

模版字符串类型还具有自动分发特性, 这是一个非常有用的特性, 可以对传入多个插槽的多个联合类型, 进行排列组合:

type phoneModel<Brand extends string, Model extends string> = `${Brand}-${Model}`

type phoneBrand = 'iphone' | 'huawei' | 'xiaomi'
type phoneVersion = '12' | '13' | '14'
type result = phoneModel<phoneBrand, phoneVersion>
/**
 * "iphone-12" | "iphone-13" | "iphone-14" | "huawei-12" | "huawei-13" |
 *  "huawei-14" | "xiaomi-12" | "xiaomi-13" | "xiaomi-14"
 */

以上案例可以看出, 手机的品牌-型号, 通过模版字符串传入变量, 可以对他们进行排列组合, 3种品牌和3种型号, 组成了9个字符串字面量类型;

如果我们不想要其中生成的几个成员, 可以使用之前介绍的差集运算, 将他们过滤掉

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

type result2 = Exclude<result, 'xiaomi-13' | 'huawei-12'>
/**
 * "iphone-12" | "iphone-13" | "iphone-14" | "huawei-13" | 
 * "huawei-14" | "xiaomi-12" | "xiaomi-14"
 */

与索引类型和映射类型配合使用

模版字符串类型同样可以和索引类型与映射类型配合使用, 例如, 可以和索引类型配合使用, 实现对一个对象字面量键名的提取和统一修改;

// 例如, 如果要修改一个对象类型的
interface EventType {
  click: (...args: any[]) => void;
  move: (...args: any[]) => void;
  scroll: (...args: any[]) => void;
}
// "onclick" | "onmove" | "onscroll"
type EventsName = `on${keyof EventType}`

以我们目前学习到的知识, 我们可以提取一个对象类型的键, 可以根据对象的键来拆分一个对象类型; 但是, 还从未修改过某个对象类型的键名, 而模版类型和重映射, 使之成为了可能, 所谓的重映射, 其部分逻辑和断言一样, 也是使用as将一个类型改为另一个类型, 而模版字符串则可以根据不同的键名, 做相同的修改操作

interface Person {
  job:string,
  clothing: string,
  shoes: string
}

type NewType<T extends object> = {
  [K in keyof T as `new-${K & string}`]: T[K]
}

type Result = NewType<Person>
/**
 * type Result = {
    "new-job": string;
    "new-clothing": string;
    "new-shoes": string;
  }
 */

在上面的案例中, 我们将对象类型的每一个键的类型K前面, 加上了new-, 想想看, 如果我们没有学习模版字符串类型, 这种修改显然是做不到的, 我们充其量可以将每个键改为string或者number这种原始类型, 但是无法灵活地, 按照某种规律去修改键! 这里需要注意的一点是, 模版字符串的插槽不接受symbol, 类型, 而keyof T的类型其实是string | number | symbol, 所以通过交叉类型 T & string来将其限定为string类型!

与内置工具类型配合使用

模版字符串还能和很多内置工具类型配合使用, 来实现对字符串字面量类型的灵活快捷的修改

interface Dog {
  color: string;
  age: number;
  breed: string;
  weight: string;
}

// 将对象类型的键全部转为大写
type toUpperCaseKeys<T extends object> = {
  [K in keyof T as `${Uppercase<K & string>}`]: T[K]
}

type Result1 = toUpperCaseKeys<Dog>
/**
 * type Result1 = {
    COLOR: string;
    AGE: number;
    BREED: string;
    WEIGHT: string;
  }
 */

以上案例中, 我们使用了模版字符串类型和UpperCase工具类型, 实现了对对象所有键的修改, 将其全部变为大写, 与之相类似的工具类型还有 LowerCaseCapitalizeUncapitalize, 其实从字面就可以理解, 它们分别是, 将字符串改为大写、首字母大写、首字母小写!

// 键名全部改为小写
type toLowerCaseKeys<T extends object> = {
  [K in keyof T as `${Lowercase<K & string>}`]: T[K]
}
type Result2 = toLowerCaseKeys<Result1>
/**
 * type Result2 = {
    color: string;
    age: number;
    breed: string;
    weight: string;
  }
*/

// 将对象类型的键首字母全部转为大写
type toCapitalizeKeys<T extends object> = {
  [K in keyof T as `${Capitalize<K & string>}`]: T[K]
}

type Result3 = toCapitalizeKeys<Dog>
/**
 * type Result3 = {
    Color: string;
    Age: number;
    Breed: string;
    Weight: string;
}
*/

// 将对象类型的键首字母全部转为小写
type toUncapitalizeKeys<T extends object> = {
  [K in keyof T as `${Uncapitalize<K & string>}`]: T[K]
}

type Result4 = toUncapitalizeKeys<Result3>
/**
 * type Result4 = {
    color: string;
    age: number;
    breed: string;
    weight: string;
}
*/

与模式匹配配合使用

通过前面的学习, 我们知道了, 如果说, 索引类型&映射类型是利用自己批量处理数据的能力在配合模版字符串类型工作, 内置工具类型利用自身标准逻辑协助字符串模版类型工作; 那么模式匹配自然就是利用自己擅长的'提取'能力, 帮助模版字符串对目标类型进行精确定位从而对目标类型进行修改;

type Revert<T> = T extends `${infer F} ${infer L}` ? `new result is: ${L} ${F}` : never;

type Result = Revert<'hello world'>
// new result is: world hello

上面的案例, 其本质是匹配到'字符串' + '空格' + '字符串' 这种格式的字符串字面量类型, 然后, 进行转换, 空格两边的字符串字面量类型部分被精确匹配, 并交给模版字符串进行修改, 要注意的是, 模版字符串在extends条件限制部分就已经在使用了, 这就是告诉程序, 这是在一个字符串中进行的匹配; 前面传入的'hello world'只有一个空格, 那如果有俩空格呢?

type Revert<T> = T extends `${infer F} ${infer L}` ? `new result is: ${L} ${F}` : never;

type Result = Revert<'hello world !'>
// new result is: world ! hello

由此可见, 此处, 只匹配了第一个空格, 即hello 和 word之间的空格, world 和 ! 其实被统一划归为了infer L的部分!