Typescript 模板字面量类型——让字符串类型更强大
本文将介绍 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
类型的变量赋值 red
或 blue
是 OK 的,赋值其他的就会报错。
类似的,模板字面量类型也可以拿 Javascript
ES6 中模板字符串来参照。
ES6:模板字符串拼接
const world = 'world'
const greeting = `hello ${world}`
TS:模板字面量
type World = "world"
type Greeting = `hello ${World}`
两者的区别是,一个是字符串(可以直接在控制台打印),一个是表示 hello world
的字符串类型(可以用于做类型声明)。
利用这个特性,我们可以减少手写类型声明,提高代码可读性,让代码变得更好维护。举个具体的例子:
使用 Popover
,Tooltip
等组件时,通常需要设置弹出层的方向,大致有👇🏻这几种。
在没有模板字面量类型的情况下,通常我们会手动枚举每一种排列组合,总共十二种。
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 也内置提供了一些泛型工具:Uppercase
, Lowercase
, Capitalize
and Uncapitalize
。
Uppercase
和 Lowercase
会对整个字符串分别做大小写转换。
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.toLowerCase
和 String.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 中取对象属性一样,T
是 a
类型,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 写起来更得心应手。
转载自:https://juejin.cn/post/7238917340209807418