TS
用 `as const` 让 TS 提示具体可以填哪些值
这篇笔记记录一下我写单选组件时碰到的一个 TypeScript 问题。 当时我有一个单选组件,选项明明已经写在数组里了,但是填写默认值的时候,编辑器只知道它是 string,不会提示具体有哪些值可以选。 后来发现,关键是给传入的数组加上 as const。这样 TypeScript 不会把数组里的内容全部放宽成普通的

这篇笔记记录一下我写单选组件时碰到的一个 TypeScript 问题。
当时我有一个单选组件,选项明明已经写在数组里了,但是填写默认值的时候,编辑器只知道它是 string,不会提示具体有哪些值可以选。
后来发现,关键是给传入的数组加上 as const。这样 TypeScript 不会把数组里的内容全部放宽成普通的 string,而是会记住每一个具体值。
我碰到的问题
先看一个简单例子:
const options = ["small", "medium", "large"];把鼠标放到 options 上,TypeScript 推导出来的是:
string[]虽然我们自己看得出来数组里只有三个值,但是 TypeScript 会认为这个数组后面还可能被修改:
options.push("extra-large");所以它不敢把类型限制得太死,只能把每一项看成普通的 string。
如果我的组件类型也是这样写:
type RadioProps = {
options: string[];
value: string;
};那么使用组件时,value 填任何字符串都不会报错:
<Radio options={options} value="随便填一个值也不会报错" />编辑器当然也没办法提示我到底有哪些值可以选。
给数组加上 as const
把数组改成:
const options = ["small", "medium", "large"] as const;这时 TypeScript 推导出来的类型会变成:
readonly ["small", "medium", "large"]这里有两个变化:
- 数组变成了只读元组,不能再随便
push新内容。 - TypeScript 记住了每一项的具体值,不再只是笼统地认为它们是
string。
然后可以用下面的写法,从数组里取出每一项的类型:
type Size = (typeof options)[number];得到的结果是:
type Size = "small" | "medium" | "large";| 表示联合类型,也就是只能从这几个值里选一个。
const a: Size = "medium"; // 正确
const b: Size = "other"; // 报错在编辑器里输入时,也会直接提示 "small"、"medium" 和 "large"。
放到单选组件里
真实项目里的选项一般不只是一个字符串,还会有展示给用户看的文字:
const sizeOptions = [
{ label: "小", value: "small" },
{ label: "中", value: "medium" },
{ label: "大", value: "large" },
] as const;这时可以这样取出所有 value:
type Size = (typeof sizeOptions)[number]["value"];拆开来看:
typeof sizeOptions先取得整个数组的类型。
(typeof sizeOptions)[number]再取得数组中任意一项的类型。
(typeof sizeOptions)[number]["value"]最后取得每一项的 value 类型,结果还是:
type Size = "small" | "medium" | "large";写一个能自动推导类型的组件
如果每写一组单选项都要手动声明一次类型,还是有点麻烦。可以让组件自己根据 options 推导出 value。
下面是一个简单的 React 版本:
type RadioOption = {
label: string;
value: string;
};
type RadioGroupProps<TOptions extends readonly RadioOption[]> = {
options: TOptions;
value: TOptions[number]["value"];
onChange: (value: TOptions[number]["value"]) => void;
};
function RadioGroup<const TOptions extends readonly RadioOption[]>({
options,
value,
onChange,
}: RadioGroupProps<TOptions>) {
return (
<div>
{options.map((option) => (
<label key={option.value}>
<input
type="radio"
checked={option.value === value}
onChange={() => onChange(option.value)}
/>
{option.label}
</label>
))}
</div>
);
}使用组件:
const sizeOptions = [
{ label: "小", value: "small" },
{ label: "中", value: "medium" },
{ label: "大", value: "large" },
] as const;
<RadioGroup
options={sizeOptions}
value="medium"
onChange={(value) => {
// value 的类型是:
// "small" | "medium" | "large"
console.log(value);
}}
/>;输入 value 时,编辑器就会提示:
"small" | "medium" | "large"如果填错了,也会直接报错:
<RadioGroup
options={sizeOptions}
value="extra-large"
onChange={() => {}}
/>;为什么组件参数要写 readonly
加上 as const 后,sizeOptions 会变成只读数组。
如果组件参数还是这样写:
options: RadioOption[];TypeScript 会报错,因为只读数组不能传给一个可能修改它的参数。
单选组件只是读取选项,并不需要修改数组,所以应该写成:
options: readonly RadioOption[];这个写法更符合组件真正做的事情。
TypeScript 5.0 以后的写法
上面的组件已经在泛型前面加了一个 const:
function RadioGroup<const TOptions extends readonly RadioOption[]>({
options,
value,
onChange,
}: RadioGroupProps<TOptions>) {
// ...
}这是 TypeScript 5.0 加入的 const 泛型参数。它会让 TypeScript 尽量保留更具体的字面量类型。
例如直接把数组写在组件里:
<RadioGroup
options={[
{ label: "小", value: "small" },
{ label: "中", value: "medium" },
{ label: "大", value: "large" },
]}
value="medium"
onChange={(value) => {
console.log(value);
}}
/>;这种情况下,也更容易推导出具体的值。
不过我觉得,选项会重复使用时,单独定义数组并写上 as const 更直观:
const sizeOptions = [
{ label: "小", value: "small" },
{ label: "中", value: "medium" },
{ label: "大", value: "large" },
] as const;as const 和 const 不是一回事
这两个写法长得有点像,但是作用不完全一样:
const options = ["small", "medium", "large"];这里的 const 只是说变量 options 不能重新赋值,但是数组内部仍然可以修改。
const options = ["small", "medium", "large"] as const;这里的 as const 会让 TypeScript 保留数组里的具体字面量,并且把数组推导为只读元组。
对比一下:
const a = ["small", "medium", "large"];
// string[]
const b = ["small", "medium", "large"] as const;
// readonly ["small", "medium", "large"]其他可以用到的地方
这个方法不只是适合单选组件。只要有“值只能从固定列表里选”的场景,都可以用:
- 下拉框。
- 标签筛选。
- 排序方式。
- 表格状态。
- 权限名称。
- 路由名称。
比如:
const sortOptions = ["newest", "oldest", "popular"] as const;
type SortType = (typeof sortOptions)[number];
function changeSort(type: SortType) {
console.log(type);
}
changeSort("newest"); // 正确
changeSort("random"); // 报错我最后记住的写法
数组里是普通字符串时:
const values = ["a", "b", "c"] as const;
type Value = (typeof values)[number];数组里是对象时:
const options = [
{ label: "选项 A", value: "a" },
{ label: "选项 B", value: "b" },
] as const;
type Value = (typeof options)[number]["value"];写组件参数时,记得兼容只读数组:
type RadioGroupProps<TOptions extends readonly RadioOption[]> = {
options: TOptions;
value: TOptions[number]["value"];
};我当时缺的就是 as const 这一小段。加上以后,TypeScript 才真正知道数组里有哪些固定值,编辑器也才能给出准确提示。