在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即字面量类型。
TypeScript 支持以下字面量类型:
- 字符串字面量类型;
- 数字字面量类型;
- 布尔字面量类型;
- 模板字面量类型。
字符串字面量类型
字符串字面量类型用来约束取值只能是某几个字符串中的一个。
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
// do something
}
handleEvent(document.getElementById('hello'), 'scroll'); // 没问题
handleEvent(document.getElementById('world'), 'dblclick'); // 报错,event 不能为 'dblclick'
// index.ts(7,47): error TS2345: Argument of type '"dblclick"' is not assignable to parameter of type 'EventNames'.
上例中,我们使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种。
数字字面量类型
数字字面量类型就是指定类型为具体的数值:
type Age = 18;
interface Info {
name: string;
age: Age;
}
const info: Info = {
name: "TS",
age: 28 // ❌ 不能将类型“28”分配给类型“18”
};
布尔字面量类型
布尔字面量类型就是指定类型为具体的布尔值(true或false):
let success: true;
let fail: false;
let value: true | false;
success = true;
success = false; // ❌ 不能将类型“false”分配给类型“true”
由于布尔字面量类型只有true或false两种,所以下面 value 变量的类型是一样的:
let value: true | false;
let value: boolean;
模板字面量类型
在 TypeScript 4.1 版本中新增了模板字面量类型。什么是模板字面量类型呢?它一字符串字面量类型为基础,可以通过联合类型扩展成多个字符串。它与 JavaScript 的模板字符串语法相同,但是只能用在类型定义中使用。
① 基本语法
当使用模板字面量类型时,它会替换模板中的变量,返回一个新的字符串字面量。
type attrs = "Phone" | "Name";
type target = `get${attrs}`;
// type target = "getPhone" | "getName";
可以看到,模板字面量类型的语法简单,并且易读且功能强大。
假如有一个CSS内边距规则的类型,定义如下:
type CssPadding = 'padding-left' | 'padding-right' | 'padding-top' | 'padding-bottom';
上面的类型是没有问题的,但是有点冗长。margin 和 padding 的规则相同,但是这样写我们无法重用任何内容,最终就会得到很多重复的代码。
下面来使用模版字面量类型来解决上面的问题:
type Direction = 'left' | 'right' | 'top' | 'bottom';
type CssPadding = `padding-${Direction}`
// type CssPadding = 'padding-left' | 'padding-right' | 'padding-top' | 'padding-bottom'
这样代码就变得更加简洁。如果想创建margin类型,就可以重用Direction类型:
type CssMargin = `margin-${Direction}`
如果在 JavaScript 中定义了变量,就可以使用 typeof 运算符来提取它:
const direction = 'left';
type CssPadding = `padding-${typeof direction}`;
// type CssPadding = "padding-left"
② 变量限制
模版字面量中的变量可以是任意的类型吗?可以使用对象或自定义类型吗?来看下面的例子:
type CustomObject = {
foo: string
}
type target = `get${CustomObject}`
// ❌ 不能将类型“CustomObject”分配给类型“string | number | bigint | boolean | null | undefined”。
type complexUnion = string | number | bigint | boolean | null | undefined;
type target2 = `get${complexUnion}` // ✅
可以看到,当在模板字面量类型中使用对象类型时,就报错了,因为编译器不知道如何将它序列化为字符串。实际上,模板字面量类型中的变量只允许是string、number、bigint、boolean、null、undefined或这些类型的联合类型。
③ 实用程序
Typescript 提供了一组实用程序来帮助处理字符串。它们不是模板字面量类型独有的,但与它们结合使用时很方便。完整列表如下:
- Uppercase
:将类型转换为大写; - Lowercase
:将类型转换为小写; - Capitalize
:将类型第一个字母大写; - Uncapitalize
:将类型第一个字母小写。
这些实用程序只接受一个字符串字面量类型作为参数,否则就会在编译时抛出错误:
type nameProperty = Uncapitalize<'Name'>;
// type nameProperty = 'name';
type upercaseDigit = Uppercase<10>;
// ❌ 类型“number”不满足约束“string”。
type property = 'phone';
type UppercaseProperty = Uppercase<property>;
// type UppercaseProperty = 'Property';
下面来看一个更复杂的场景,将字符串字面量类型与这些实用程序结合使用。将两种类型进行组合,并将第二种类型的首字母大小,这样组合之后的类型符合驼峰命名法:
type actions = 'add' | 'remove';
type property = 'name' | 'phone';
type result = `${actions}${Capitalize<property>}`;
// type result = addName | addPhone | removeName | removePhone
④ 类型推断
在上面的例子中,我们使用使用模版字面量类型将现有的类型组合成新类型。下面来看看如何使用模板字面量类型从组合的类型中提取类型。这里就需要用到infer关键字,它允许我们从条件类型中的另一个类型推断出一个类型。
下面来尝试提取字符串字面量 marginRight 的根节点:
type Direction = 'left' | 'right' | 'top' | 'bottom';
type InferRoot<T> = T extends `${infer K}${Capitalize<Direction>}` ? K : T;
type Result1 = InferRoot<'marginRight'>;
// type Result1 = 'margin';
type Result2 = InferRoot<'paddingLeft'>;
// type Result2 = 'padding';
可以看到, 模版字面量还是很强大的,不仅可以创建类型,还可以解构它们。
⑤ 作为判别式
在TypeScript 4.5 版本中,支持了将模板字面量串类型作为判别式,用于类型推导。来看下面的例子:
interface Message {
type: string;
url: string;
}
interface SuccessMessage extends Message {
type: `${string}Success`;
body: string;
}
interface ErrorMessage extends Message {
type: `${string}Error`;
message: string;
}
function handler(r: SuccessMessage | ErrorMessage) {
if (r.type === "HttpSuccess") {
let token = r.body;
}
}
在这个例子中,handler 函数中的 r 的类型就被推断为 SuccessMessage。因为根据 SuccessMessage 和 ErrorMessage 类型中的type字段的模板字面量类型推断出 HttpSucces 是根据SuccessMessage中的type创建的。