Menu

TypeScript 字面量类型与 as const 用法详解

字面量类型是只有一个值的类型,比如 "GET" 或 404。本文讲解字符串、数字和布尔字面量类型,字面量联合类型,为什么 let 会拓宽类型而 const 不会,as const 的作用,以及 const 类型参数。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

字面量类型是只有一个值的类型:"up" 这个类型唯一的成员是字符串 "up",404 这个类型唯一的成员是数字 404。单独使用时它们用处不大,但组合成联合类型后,就能得到一个只接受一组固定值、其他值一律拒绝的变量。

如果没有 @ts-expect-error 注释,最后一次调用会产生编译错误(TS2345)。加上注释后,程序可以编译,这次调用照样会运行并打印 moving north by 1:字面量类型只存在于编译器中,运行时这个值就是一个普通字符串。

字符串、数字和布尔字面量

任何字符串、数字、bigint 或布尔值都可以写成类型,编译器随后只接受这个确切的值。

let method: "GET" = "GET";
let code: 404 = 404;
let ok: true = true;
let big: 10n = 10n;

type Port = 80 | 443 | 8080;
const port: Port = 443;

boolean 本身就是联合类型 true | false,所以用 if (flag) 收窄一个 boolean 之后,else 分支里剩下的就是 false。

字面量类型允许的值更宽的类型
"GET"只有字符串 "GET"string
404只有数字 404number
10n只有 bigint 10bigint
true只有 trueboolean

字面量联合类型

最常见的用法是用联合类型列出所有允许的值。在函数内部,编译器会随着你的检查收窄联合类型,所以每个分支都确切知道自己拿到的是哪个值。

字符串字面量联合类型是 TypeScript 中替代枚举的常用做法。它没有任何运行时开销,值就是普通字符串,可以直接打印、通过 JSON 传输,而写错一个字母就是编译错误。两者的取舍详见枚举。

类型拓宽:let 与 const

TypeScript 根据字面量推断类型时,会看这个值能不能改变。const 变量永远不能被重新赋值,所以保留字面量类型;let 变量可以,所以它的类型会被拓宽为通用类型。

在编辑器中把鼠标悬停在每个名称上,就能看到推断出的类型。如果你想要一个只能保存特定值的 let,就给它加上标注:let mode: "light" | "dark" = "light"。

为什么对象属性会被拓宽

对象字面量的属性是可变的,所以即使对象存放在 const 中,属性也会被拓宽。这是最常见的意外碰上字面量类型的情形:

编译器报告:

index.ts(7,15): error TS2345: Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.

req 被推断为 { url: string; method: string },因为之后的代码可能会执行 req.method = "DELETE"。有三种修复方法:

第四种办法是 satisfies,它会按某个类型检查对象,同时保留属性的字面量类型。

as const

as const 是 const 断言。把它写在表达式后面,编译器就会推断出尽可能窄的类型:

  • 字符串、数字和布尔值保留字面量类型
  • 对象属性变为 readonly
  • 数组字面量变为长度固定的 readonly 元组

这个断言只在编译时起作用。生成的 JavaScript 就是去掉 as const 的同一个对象字面量,所以运行时没有什么能阻止其他代码修改它。如果需要运行时的保证,再调用一次 Object.freeze。

从 as const 数组得到联合类型

一个常见的模式是把允许的值放在一个数组里(运行时可以遍历它),再从中推导出联合类型。(typeof arr)[number] 的意思是“arr 中任意一个元素的类型”。

不加 as const 的话,ROLES 会是 string[],Role 也就只是普通的 string。isRole 中需要转换成 readonly string[],是因为字面量元组上的 includes 只接受这些字面量,而这个函数的目的恰恰是检查一个可能不在其中的字符串。同样的模式也适用于键值映射:const Status = { Active: "active", Banned: "banned" } as const,然后 type Status = (typeof Status)[keyof typeof Status]。

const 类型参数

泛型函数通常会拓宽你传入的字面量。从 TypeScript 5.0 开始,你可以把类型参数标记为 const,编译器就会像实参带了 as const 一样推断它,而不需要调用方自己写。

这主要是给库作者用的工具:路由定义、构建器和 schema 辅助函数都用它,让调用方从普通字面量中得到精确的类型。

const 的几种含义

关键字 const 在 TypeScript 代码中会出现在四个不同的地方:

语法类别作用
const x = 1JavaScript 声明绑定不能被重新赋值;字面量值保留字面量类型
expr as constTypeScript 断言最窄的类型:字面量、readonly 属性、只读元组
function f<const T>()TypeScript 类型参数像带了 as const 一样推断实参
const enum E {}TypeScript 枚举成员在编译时被内联的枚举

它们都不会在运行时冻结对象。const obj = { a: 1 } 仍然允许 obj.a = 2,只有重新给 obj 本身赋值才会报错。

常见问题

TypeScript 中的字面量类型是什么?

只允许一个值的类型。"GET" 这个类型唯一的值就是字符串 "GET",404 这个类型唯一的值是数字 404,true 这个类型唯一的值是 true。它们最常见的用法是组合成联合类型,比如 type Method = "GET" | "POST"。

TypeScript 中的 as const 有什么作用?

as const 是 const 断言。它让编译器为表达式推断出最窄的类型:字符串和数字值保留字面量类型,对象属性变为 readonly,数组字面量变为只读元组。它只改变类型;运行时的值仍是同一个普通对象或数组,并没有被冻结。

为什么 TypeScript 推断出的是 string,而不是我写的字面量?

因为这个值是可变的。let x = "a" 以及 { method: "GET" } 里的属性之后都可以被重新赋值,所以 TypeScript 把它们拓宽为 string。const 变量则保留字面量类型 "a"。要在对象里保留字面量,可以给它标注字面量类型、使用 as const,或者使用 satisfies。

const 和 as const 有什么区别?

const 是 JavaScript 的声明:变量不能被重新赋值,但它指向的对象仍然可以修改。as const 是 TypeScript 的类型断言:它在类型系统中把值的每个属性都变成 readonly 和字面量。两者都不会在运行时冻结对象;需要冻结请用 Object.freeze。

如何从字符串数组得到联合类型?

用 as const 声明数组,再用 number 索引它的类型:const roles = ["admin", "user"] as const; type Role = (typeof roles)[number]; 得到 "admin" | "user"。不加 as const 时数组是 string[],结果就只是 string。

Coddy 编程语言插图

和 Coddy 一起学编程

开始