在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即字面量类型。

TypeScript 支持以下字面量类型:

  • 字符串字面量类型;
  • 数字字面量类型;
  • 布尔字面量类型;
  • 模板字面量类型。

注意,类型别名与字面量类型都是使用 type 进行定义。

字符串字面量类型

字符串字面量类型用来约束取值只能是某几个字符串中的一个。

  1. type EventNames = 'click' | 'scroll' | 'mousemove';
  2. function handleEvent(ele: Element, event: EventNames) {
  3. // do something
  4. }
  5. handleEvent(document.getElementById('hello'), 'scroll'); // 没问题
  6. handleEvent(document.getElementById('world'), 'dblclick'); // 报错,event 不能为 'dblclick'
  7. // index.ts(7,47): error TS2345: Argument of type '"dblclick"' is not assignable to parameter of type 'EventNames'.

上例中,我们使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种。

数字字面量类型

数字字面量类型就是指定类型为具体的数值:

  1. type Age = 18;
  2. interface Info {
  3. name: string;
  4. age: Age;
  5. }
  6. const info: Info = {
  7. name: "TS",
  8. age: 28 // ❌ 不能将类型“28”分配给类型“18”
  9. };

布尔字面量类型

布尔字面量类型就是指定类型为具体的布尔值(true或false):

  1. let success: true;
  2. let fail: false;
  3. let value: true | false;
  4. success = true;
  5. success = false; // ❌ 不能将类型“false”分配给类型“true”

由于布尔字面量类型只有true或false两种,所以下面 value 变量的类型是一样的:

  1. let value: true | false;
  2. let value: boolean;

模板字面量类型

在 TypeScript 4.1 版本中新增了模板字面量类型。什么是模板字面量类型呢?它一字符串字面量类型为基础,可以通过联合类型扩展成多个字符串。它与 JavaScript 的模板字符串语法相同,但是只能用在类型定义中使用。

① 基本语法

当使用模板字面量类型时,它会替换模板中的变量,返回一个新的字符串字面量。

  1. type attrs = "Phone" | "Name";
  2. type target = `get${attrs}`;
  3. // type target = "getPhone" | "getName";

可以看到,模板字面量类型的语法简单,并且易读且功能强大。
假如有一个CSS内边距规则的类型,定义如下:

  1. type CssPadding = 'padding-left' | 'padding-right' | 'padding-top' | 'padding-bottom';

上面的类型是没有问题的,但是有点冗长。margin 和 padding 的规则相同,但是这样写我们无法重用任何内容,最终就会得到很多重复的代码。
下面来使用模版字面量类型来解决上面的问题:

  1. type Direction = 'left' | 'right' | 'top' | 'bottom';
  2. type CssPadding = `padding-${Direction}`
  3. // type CssPadding = 'padding-left' | 'padding-right' | 'padding-top' | 'padding-bottom'

这样代码就变得更加简洁。如果想创建margin类型,就可以重用Direction类型:

  1. type CssMargin = `margin-${Direction}`

如果在 JavaScript 中定义了变量,就可以使用 typeof 运算符来提取它:

  1. const direction = 'left';
  2. type CssPadding = `padding-${typeof direction}`;
  3. // type CssPadding = "padding-left"

② 变量限制

模版字面量中的变量可以是任意的类型吗?可以使用对象或自定义类型吗?来看下面的例子:

  1. type CustomObject = {
  2. foo: string
  3. }
  4. type target = `get${CustomObject}`
  5. // ❌ 不能将类型“CustomObject”分配给类型“string | number | bigint | boolean | null | undefined”。
  6. type complexUnion = string | number | bigint | boolean | null | undefined;
  7. type target2 = `get${complexUnion}` // ✅

可以看到,当在模板字面量类型中使用对象类型时,就报错了,因为编译器不知道如何将它序列化为字符串。实际上,模板字面量类型中的变量只允许是string、number、bigint、boolean、null、undefined或这些类型的联合类型。

③ 实用程序

Typescript 提供了一组实用程序来帮助处理字符串。它们不是模板字面量类型独有的,但与它们结合使用时很方便。完整列表如下:

  • Uppercase:将类型转换为大写;
  • Lowercase:将类型转换为小写;
  • Capitalize:将类型第一个字母大写;
  • Uncapitalize:将类型第一个字母小写。

这些实用程序只接受一个字符串字面量类型作为参数,否则就会在编译时抛出错误:

  1. type nameProperty = Uncapitalize<'Name'>;
  2. // type nameProperty = 'name';
  3. type upercaseDigit = Uppercase<10>;
  4. // ❌ 类型“number”不满足约束“string”。
  5. type property = 'phone';
  6. type UppercaseProperty = Uppercase<property>;
  7. // type UppercaseProperty = 'Property';

下面来看一个更复杂的场景,将字符串字面量类型与这些实用程序结合使用。将两种类型进行组合,并将第二种类型的首字母大小,这样组合之后的类型符合驼峰命名法:

  1. type actions = 'add' | 'remove';
  2. type property = 'name' | 'phone';
  3. type result = `${actions}${Capitalize<property>}`;
  4. // type result = addName | addPhone | removeName | removePhone

④ 类型推断

在上面的例子中,我们使用使用模版字面量类型将现有的类型组合成新类型。下面来看看如何使用模板字面量类型从组合的类型中提取类型。这里就需要用到infer关键字,它允许我们从条件类型中的另一个类型推断出一个类型。
下面来尝试提取字符串字面量 marginRight 的根节点:

  1. type Direction = 'left' | 'right' | 'top' | 'bottom';
  2. type InferRoot<T> = T extends `${infer K}${Capitalize<Direction>}` ? K : T;
  3. type Result1 = InferRoot<'marginRight'>;
  4. // type Result1 = 'margin';
  5. type Result2 = InferRoot<'paddingLeft'>;
  6. // type Result2 = 'padding';

可以看到, 模版字面量还是很强大的,不仅可以创建类型,还可以解构它们。

⑤ 作为判别式

在TypeScript 4.5 版本中,支持了将模板字面量串类型作为判别式,用于类型推导。来看下面的例子:

  1. interface Message {
  2. type: string;
  3. url: string;
  4. }
  5. interface SuccessMessage extends Message {
  6. type: `${string}Success`;
  7. body: string;
  8. }
  9. interface ErrorMessage extends Message {
  10. type: `${string}Error`;
  11. message: string;
  12. }
  13. function handler(r: SuccessMessage | ErrorMessage) {
  14. if (r.type === "HttpSuccess") {
  15. let token = r.body;
  16. }
  17. }

在这个例子中,handler 函数中的 r 的类型就被推断为 SuccessMessage。因为根据 SuccessMessage 和 ErrorMessage 类型中的type字段的模板字面量类型推断出 HttpSucces 是根据SuccessMessage中的type创建的。