likes
comments
collection
share

Typescript 模板字面量类型——让字符串类型更强大

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

本文将介绍 Typescript 中的模板字面量类型(Template Literal Types)特性及其如何在开发工作中应用。

模板字面量类型

模板字面量类型是在 TypeScript 4.1 中引入的一个新特性,在那之前我们只有字面量类型,但是没有模板能力。

字符串字面量类型类似于 Javascirpt 中的纯字符串,不同的是,它是“类型”而不是一个值。

type Color = "red" | "blue";

const color1: Color = 'red' // ok
const color2: Color = 'yellow' // 报错

Color 类型表示字符串是 red 类型或者是 blue 类型,给一个 Color 类型的变量赋值 redblue 是 OK 的,赋值其他的就会报错。

类似的,模板字面量类型也可以拿 Javascript ES6 中模板字符串来参照。

ES6:模板字符串拼接

const world = 'world'
const greeting = `hello ${world}`

TS:模板字面量

type World = "world"
type Greeting = `hello ${World}`

两者的区别是,一个是字符串(可以直接在控制台打印),一个是表示 hello world 的字符串类型(可以用于做类型声明)。

利用这个特性,我们可以减少手写类型声明,提高代码可读性,让代码变得更好维护。举个具体的例子:

使用 PopoverTooltip 等组件时,通常需要设置弹出层的方向,大致有👇🏻这几种。

Typescript 模板字面量类型——让字符串类型更强大

在没有模板字面量类型的情况下,通常我们会手动枚举每一种排列组合,总共十二种。

type Placement = 'top' | 'left' | 'right' | 'bottom'
  | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'left-top'
  | 'left-bottom' | 'right-top' | 'right-bottom'

使用这个特性,我们就可以让 TS 来帮我们做排列组合。

type Horizontal = "left" | "right"
type Vertical = "top" | "bottom"
type Placement = `${Vertical}-${Horizontal}` | `${Horizontal}-${Vertical}` 
    | Vertical | Horizontal

前后两个 Placement 类型是等价的,好处是我们不需要手写那么多代码了。这个例子比较简单,相信你看完之后就明白模板字面量类型是如何工作的。

内置泛型工具

在 4.1 版本中,TS 也内置提供了一些泛型工具:UppercaseLowercaseCapitalize and Uncapitalize

UppercaseLowercase 会对整个字符串分别做大小写转换。

type Greeting = "Hello, world"

type Str1 = Uppercase<Greeting>
// HELLO, WORLD
type Str2 = Lowercase<Greeting>
// hello, world

Capitalize and Uncapitalize 则仅对字符串的第一个字符做转换。

type LowercaseGreeting = "hello, world"
type Str1 = Capitalize<LowercaseGreeting>
// Hello, world

type UppercaseGreeting = "HELLO, WORLD";
type Str2 = Uncapitalize<UppercaseGreeting>;
// hELLO, WORLD

光是这么一说,可能还是记不住,接下来参照 《结合实例学习 Typescript》 里的做法,我们自己动手将这两组泛型实现一下。

自行实现

Capitalize / Uncapitalize

首先 Capitalize/Uncapitalize 其实是可以从 Uppercase/Lowercase 推断出来的。

type MyCapitalize<T extends string> = 
    T extends `${infer V}${infer U}` ? `${Uppercase<V>}${U}` : T;

在上面这个例子中,我们定义了一个泛型方法 MyCapitalize,支持一个 string 类型的泛型参数 T,接着使用三元运算符,判断 T 是否满足 ${infer V}${infer U} 类型。如果是,将第一个字母转成大写,拼接剩下的部分。如果不是,直接返回 T

exntends 关键字可以用来判断 A 类型是否是 B 类型的子集,放在泛型参数上可以用来限制类型,放在三元运算上可以起到分类的作用。

infer 关键字则是让 TS 做动态类型推断,在这个例子中,模板字面量按照懒惰模式工作,${infer V}${infer U} 会将字符串拆成第一个字符 V + 剩余的子字符串 U。如果只传入一个字符,也是能工作的,这时候 V 是你的字符,U 则是空字符串。

Uppercase / Lowercase

那大小写怎么处理呢?在 JavaScript 中,我们通常会使用 String.prototype.toLowerCaseString.prototype.toUpperCase 来进行字符的大小写转换,那倘若不使用这两个方法呢?

直接映射不就完了。

const upperCaseMap = {
  a: "A",
  b: "B",
  c: "C",
  ...
}
upperCaseMap['a'] // A

那在 Typescript 类型系统中,我们也能使用同样的方式来实现。

Uppercase 为例,先定义一个小写转大写的类型字典。

type UpperCaseMap = {
  a: "A"
  b: "B"
  c: "C"
  ...
}

定义一个将单个字符转大写的泛型工具:

type CharUppercase<T extends string> = 
    T extends keyof UpperCaseMap ? UpperCaseMap[T] : T

keyof UpperCaseMap 的结果是 UpperCaseMap 里所有 key 组成成字符串类型并集,也就是 a | b | c...

UpperCaseMap[T] 和 JS 中取对象属性一样,Ta 类型,UpperCaseMap[T] 就会是 A 类型。

那到这里,结果就呼之欲出了,结合上面讲 Capitalize / Uncapitalize 的实现,我们将每一个字符拆出来转大写,剩下的子串继续递归。最后将结果合并就完成将全部字符转大写的工作。

type MyUppercase<T extends string> = 
    T extends `${infer H}${infer R}` ? `${CharUppercase<H>}${MyUppercase<R>}` : T;

更多使用场景

还有很多场景可能会用到模板字面量类型。例如国际化多语言zh_cn.xxx.xx, en.xxx.xx...),多主题light-button, dark-button...),样式单位${number}%, ${number}px...),事件监听onXXXChange, onXXXInput...),还有传统的 Case Converter(大驼峰,小驼峰,蛇形命名) 的工作等等。用好它可以让你的 TS 写起来更得心应手。