- 文档
- 教程
【免费下载链接】TypeScript
TypeScript 使用手册(中文版)翻译。http://www.typescriptlang.org
本篇指南以仓库 zh/handbook/literal-types.md 为骨架展开,系统讲解 TypeScript 的字面量类型(字符串、数字、布尔三种集合)、字面量收窄机制,以及它们与联合类型、类型守卫、函数重载结合后的实战用法;并结合仓库内 联合类型和交叉类型、枚举、模版字面量类型 等姊妹章节,让你掌握从"约束单一值"到"构建可区分联合做穷尽性检查"的完整技能。
什么是字面量类型
一个字面量(literal)是一个集体类型中更为具体的一种子类型。意思是:"Hello World"是一个string,但一个string并不是类型系统中的"Hello World"。
// "Hello World" 这个具体值属于 string 类型 // 但 string 类型涵盖了无穷多个可能的值,并不等于 "Hello World"目前 TypeScript 中有三种可用的字面量类型集合,分别是:
| 字面量类型集合 | 示例 | 含义 |
|---|---|---|
| 字符串字面量类型 | "ease-in" | 只允许这一个具体的字符串 |
| 数字字面量类型 | 8、16 | 只允许这一个具体的数字 |
| 布尔字面量类型 | true、false | 只允许这一个具体的布尔值 |
通过使用字面量类型,你可以规定一个字符串、数字或布尔值必须含有的确定值。这比笼统地写string/number/boolean更精确,是 TypeScript 类型系统"收窄(narrowing)"能力的基础。
字面量收窄(Literal Narrowing)
当你通过var或let来声明一个变量时,实际上你在告诉编译器:这个变量中的内容有可能会被改变。与之相对地,用const来声明对象,会让 TypeScript 知道这个对象永远不会被改变。
// 我们通过 const 保证这个变量 helloWorld 永远不会改变, // 所以 TypeScript 将它的类型设置为 "Hello World" 而不是 string。 const helloWorld = "Hello World"; // 另一方面,let 声明的变量可以改变, // 因此编译器将其类型推断为更宽泛的 string。 let hiWorld = "Hi World";这段代码背后是 TypeScript 的类型推断规则:
const声明的变量不可重新赋值,类型系统可以安全地采用最具体的字面量类型,即"Hello World";let声明的变量随时可能被重新赋值为其他字符串,因此只能退而求其次,采用其公共超类型string。
从无穷多种可能的取值(string变量的值有无穷多种)到一个更小、确定数量的取值集合(上例中"Hello World"的可能值只有一种)的过程,就叫收窄(narrowing)。字面量收窄是后续所有字面量类型应用的前提:正是因为有"具体值可以被当作类型"这一机制,"ease-in" | "ease-out"这类联合类型才能成立。
补充阅读:关于
let与var的取舍,以及const的更多细节,可参考仓库 基础类型 中"关于let"一节。
字符串字面量类型
字面量类型可以通过联合类型(union)、类型守卫(type guard)、类型别名(type alias)来结合实际字符串值。通过这些特性,我们可以让一个字符串表现得像枚举(enum)一样:只接受一组预先约定的取值。
用联合约束参数取值
type Easing = "ease-in" | "ease-out" | "ease-in-out"; class UIElement { animate(dx: number, dy: number, easing: Easing) { if (easing === "ease-in") { // ... } else if (easing === "ease-out") { // ... } else if (easing === "ease-in-out") { // ... } else { // 如果有人无视类型约束,仍有可能走到这里。 } } } let button = new UIElement(); button.animate(0, 0, "ease-in"); button.animate(0, 0, "uneasy"); // Error: Argument of type '"uneasy"' is not assignable to parameter of type 'Easing'.你可以传递三种允许的字符串,但如果传递其他字符串,编译器会在编译期报错:
Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'错误信息中会完整列出所有允许的取值,这既是约束,也是一种自我文档化——调用者一看便知该参数支持哪些合法值。
用字符串字面量实现函数重载
字符串字面量还可以用来为同一个函数按不同参数值分别声明重载(overload):
function createElement(tagName: "img"): HTMLImageElement; function createElement(tagName: "input"): HTMLInputElement; // ... 更多重载 ... function createElement(tagName: string): Element { // ... 实现代码 ... }当调用createElement("img")时,编译器匹配到第一个重载签名,返回值类型为HTMLImageElement;调用createElement("input")时匹配第二个,返回HTMLInputElement。字符串字面量在此处充当了编译期"路由",让同一函数在不同实参下返回不同类型的精确结果。重载的完整机制可进一步参考仓库 函数 一章。
字符串字面量联合 vs. 枚举
原文档指出字面量联合可以"使字符串有类似枚举(enum)的行为"。两者对比如下:
| 维度 | 字符串字面量联合 | 字符串枚举 |
|---|---|---|
| 运行时产物 | 无,纯编译期类型 | 会生成真实对象 |
| 取值约束 | "ease-in" \| "ease-out" | Direction.Up等成员 |
| 适用场景 | 简单、固定的几组取值 | 需要序列化、反向映射或成员名语义 |
正如仓库 枚举 所述:字符串枚举每个成员必须用字符串字面量或另一个字符串枚举成员初始化,且运行时存在有意义的值、便于调试;而字面量联合类型是"无运行时开销"的纯类型方案,适合不需要对象语义的场景。
数字字面量类型
TypeScript 还有数字字面量类型,它的行为和字符串字面量类型相同:只允许特定的数字取值。
function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 { return (Math.floor(Math.random() * 6) + 1) as 1 | 2 | 3 | 4 | 5 | 6; } const result = rollDice();这里的返回值类型1 | 2 | 3 | 4 | 5 | 6精确描述了骰子所有可能的点数。需要注意:
Math.random()返回类型是number,与1 | 2 | 3 | ...不兼容,因此原文档使用as断言(类型断言)来明确告知编译器"我确定这个值落在合法范围内";- 关于
as断言(以及 JSX 中只能使用as语法)的说明,可参考仓库 基础类型 的"类型断言"一节; - 这种写法隐含了"
result一定落在六个取值之内"这一约束,后续对result做switch或比较时,类型系统会利用这一点进行穷尽性检查。
数字字面量描述配置值
数字字面量类型经常用来描述配置值——比number更精确地声明"只允许这些档位":
interface MapConfig { lng: number; lat: number; tileSize: 8 | 16 | 32; } setupMap({ lng: -73.935242, lat: 40.73061, tileSize: 16 });这里tileSize只能取8、16、32三个值。如果传入24,编译器会直接报错,从而在编译期拦截非法配置,而不是等运行时地图渲染出错。这在配置驱动型应用(地图切片、图片缩放档位、日志级别等)中非常实用。
布尔字面量类型
TypeScript 还有布尔值字面量类型。你可以用它来约束某些属性之间互有关联的对象——这正是"可区分联合"(discriminated union)的雏形:
interface ValidationSuccess { isValid: true; reason: null; }; interface ValidationFailure { isValid: false; reason: string; }; type ValidationResult = | ValidationSuccess | ValidationFailure;在这个模型中:
ValidationSuccess明确声明isValid: true(而非boolean),同时reason为null;ValidationFailure声明isValid: false,reason为string;ValidationResult是两者的联合。
当拿到一个ValidationResult值时,只要判断isValid的真假,TypeScript 就能收窄出当前到底是成功还是失败分支——若是成功分支,reason一定为null;若是失败分支,reason一定是string。这样,对象内部各属性之间的一致性(如"校验通过时不该有失败原因")在类型层面被强制保证,运行时根本不会出现"isValid 为 true 却还带着错误信息"的矛盾状态。
进阶实战:从字面量到可区分联合与穷尽性检查
字面量类型最大的价值,在于与联合类型组合后构建可区分联合,再配合switch实现穷尽性检查。仓库 联合类型和交叉类型 给出了完整示例:
type NetworkLoadingState = { state: "loading" }; type NetworkFailedState = { state: "failed"; code: number }; type NetworkSuccessState = { state: "success"; response: { title: string; duration: number; summary: string }; }; type NetworkState = | NetworkLoadingState | NetworkFailedState | NetworkSuccessState; function logger(state: NetworkState): string { // 在收窄之前,不能访问非公共属性(如 state.code)——编译器会报错。 // 通过 switch 判别 state 字段(其类型是字符串字面量), // TypeScript 在代码流分析中逐步缩小联合范围: switch (state.state) { case "loading": return "Downloading..."; case "failed": // 此时类型必为 NetworkFailedState,访问 code 是安全的。 return `Error ${state.code} downloading`; case "success": return `Downloaded ${state.response.title} - ${state.response.summary}`; } }要点解析:
- 每个分支类型共享一个判别字段
state,其值分别是"loading"、"failed"、"success"这样的字符串字面量类型; - 在
switch (state.state)中把state与这些字面量比较时,TypeScript 会精确推断出当前分支的具体类型(如NetworkFailedState),从而允许安全访问code、response等非公共属性; - 这正是布尔字面量类型一节中"属性之间互有关联的对象"约束的泛化版。
穷尽性检查:never与assertNever
如果后续给NetworkState增加了新分支(例如state: "from_cache"),而logger的switch没有覆盖,会出现什么?有两种主流做法:
方式一:配合strictNullChecks指定返回类型
function logger(s: NetworkState): string { switch (s.state) { case "loading": return "loading request"; case "failed": return `failed with code ${s.code}`; case "success": return "got response"; // 缺少 "from_cache" 分支时,函数可能返回 undefined, // 与声明的 string 返回类型冲突,编译器报错。 } }因为switch不再穷尽,TypeScript 会推断函数有时返回undefined,与显式返回类型string冲突而报错。此方法比较微妙,且strictNullChecks不一定对旧代码生效。
方式二:用never类型做显式穷尽检查
function assertNever(x: never): never { throw new Error("Unexpected object: " + x); } function logger(s: NetworkState): string { switch (s.state) { case "loading": return "loading request"; case "failed": return `failed with code ${s.code}`; case "success": return "got response"; default: // 所有合法分支都被移除后,剩余类型应为 never; // 若忘了处理某个分支,s 会是真实类型,这里就会报错。 return assertNever(s); } }assertNever检查s是否属于never类型(即所有其他情况都被移除后剩下的类型)。如果忘记某个分支,s将保有一个实际类型,从而产生类型错误,并且错误信息中包含丢失的类型名称,非常直观。
再进一步:模板字面量类型
字符串字面量类型还衍生出了更强大的模板字面量类型(Template Literal Types),仓库 模版字面量类型 指出它"以字符串字面量类型为基础,且可以展开为多个字符串类型的联合类型",从 TypeScript 4.1 开始支持:
type World = 'world'; type Greeting = `hello ${World}`; // 'hello world' type EmailLocaleIDs = 'welcome_email' | 'email_heading'; type FooterLocaleIDs = 'footer_title' | 'footer_sendoff'; type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`; // "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"当替换位置是联合类型时,结果类型是各成员组合出的字符串字面量集合;多个替换位置还会交叉相乘。模板字面量类型常用于"基于属性名派生事件名"(如firstNameChanged)等场景,是字面量类型体系的重要延伸,感兴趣可继续阅读该章节。
小结与学习路径
字面量类型是 TypeScript 类型系统的核心基石,本文覆盖了:
- 收窄:
const产生最具体的字面量类型,let/var退化为宽泛类型; - 三类字面量:字符串、数字、布尔字面量类型的定义与典型应用(参数约束、函数重载、配置值、判别对象);
- 组合威力:字面量联合 + 可区分联合 +
switch+never实现编译期穷尽性检查; - 延伸方向:模板字面量类型在字符串字面量基础上的进一步扩展。
本仓库对应的官方手册章节按以下顺序递进,建议按序研读以获得完整体系:
- 基础类型:
let/const、类型断言、null/undefined等前置知识; - 字面量类型(本文所依据的原始文档);
- 联合类型和交叉类型:可区分联合与穷尽性检查的完整实现;
- 枚举:对比字符串字面量联合与枚举的取舍;
- 模版字面量类型:字符串字面量类型的进阶形态。
- 文档
- 教程
【免费下载链接】TypeScript
TypeScript 使用手册(中文版)翻译。http://www.typescriptlang.org
相关推荐
TypeScript 字面量类型(Literal Types)实战指南:精确值约束、字符串枚举与可辨识联合
TypeScript 字面量类型(Literal Types)实战指南:精确值约束、字符串枚举与可辨识联合 字面量类型(Literal Types)是 Type
教程TypeScript 字面量类型(Literal Types)完全指南:从单元素集合到联合、收窄与实战
TypeScript 字面量类型(Literal Types)完全指南:从单元素集合到联合、收窄与实战 本文以《The Concise TypeScript B
文档教程TypeScript 字面量类型(Literal Types)完全指南:从精确值约束到联合类型实战
TypeScript 字面量类型(Literal Types)完全指南:从精确值约束到联合类型实战 导读 :字面量类型(Literal Types)是 Type
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考