TS

用 `as const` 让 TS 提示具体可以填哪些值

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

2026年6月13日9 分钟阅读
用 `as const` 让 TS 提示具体可以填哪些值

这篇笔记记录一下我写单选组件时碰到的一个 TypeScript 问题。

当时我有一个单选组件,选项明明已经写在数组里了,但是填写默认值的时候,编辑器只知道它是 string,不会提示具体有哪些值可以选。

后来发现,关键是给传入的数组加上 as const。这样 TypeScript 不会把数组里的内容全部放宽成普通的 string,而是会记住每一个具体值。

我碰到的问题

先看一个简单例子:

ts
const options = ["small", "medium", "large"];

把鼠标放到 options 上,TypeScript 推导出来的是:

ts
string[]

虽然我们自己看得出来数组里只有三个值,但是 TypeScript 会认为这个数组后面还可能被修改:

ts
options.push("extra-large");

所以它不敢把类型限制得太死,只能把每一项看成普通的 string

如果我的组件类型也是这样写:

ts
type RadioProps = {
  options: string[];
  value: string;
};

那么使用组件时,value 填任何字符串都不会报错:

tsx
<Radio options={options} value="随便填一个值也不会报错" />

编辑器当然也没办法提示我到底有哪些值可以选。

给数组加上 as const

把数组改成:

ts
const options = ["small", "medium", "large"] as const;

这时 TypeScript 推导出来的类型会变成:

ts
readonly ["small", "medium", "large"]

这里有两个变化:

  • 数组变成了只读元组,不能再随便 push 新内容。
  • TypeScript 记住了每一项的具体值,不再只是笼统地认为它们是 string

然后可以用下面的写法,从数组里取出每一项的类型:

ts
type Size = (typeof options)[number];

得到的结果是:

ts
type Size = "small" | "medium" | "large";

| 表示联合类型,也就是只能从这几个值里选一个。

ts
const a: Size = "medium"; // 正确
const b: Size = "other"; // 报错

在编辑器里输入时,也会直接提示 "small""medium""large"

放到单选组件里

真实项目里的选项一般不只是一个字符串,还会有展示给用户看的文字:

ts
const sizeOptions = [
  { label: "小", value: "small" },
  { label: "中", value: "medium" },
  { label: "大", value: "large" },
] as const;

这时可以这样取出所有 value

ts
type Size = (typeof sizeOptions)[number]["value"];

拆开来看:

ts
typeof sizeOptions

先取得整个数组的类型。

ts
(typeof sizeOptions)[number]

再取得数组中任意一项的类型。

ts
(typeof sizeOptions)[number]["value"]

最后取得每一项的 value 类型,结果还是:

ts
type Size = "small" | "medium" | "large";

写一个能自动推导类型的组件

如果每写一组单选项都要手动声明一次类型,还是有点麻烦。可以让组件自己根据 options 推导出 value

下面是一个简单的 React 版本:

tsx
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>
  );
}

使用组件:

tsx
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 时,编辑器就会提示:

text
"small" | "medium" | "large"

如果填错了,也会直接报错:

tsx
<RadioGroup
  options={sizeOptions}
  value="extra-large"
  onChange={() => {}}
/>;

为什么组件参数要写 readonly

加上 as const 后,sizeOptions 会变成只读数组。

如果组件参数还是这样写:

ts
options: RadioOption[];

TypeScript 会报错,因为只读数组不能传给一个可能修改它的参数。

单选组件只是读取选项,并不需要修改数组,所以应该写成:

ts
options: readonly RadioOption[];

这个写法更符合组件真正做的事情。

TypeScript 5.0 以后的写法

上面的组件已经在泛型前面加了一个 const

tsx
function RadioGroup<const TOptions extends readonly RadioOption[]>({
  options,
  value,
  onChange,
}: RadioGroupProps<TOptions>) {
  // ...
}

这是 TypeScript 5.0 加入的 const 泛型参数。它会让 TypeScript 尽量保留更具体的字面量类型。

例如直接把数组写在组件里:

tsx
<RadioGroup
  options={[
    { label: "小", value: "small" },
    { label: "中", value: "medium" },
    { label: "大", value: "large" },
  ]}
  value="medium"
  onChange={(value) => {
    console.log(value);
  }}
/>;

这种情况下,也更容易推导出具体的值。

不过我觉得,选项会重复使用时,单独定义数组并写上 as const 更直观:

ts
const sizeOptions = [
  { label: "小", value: "small" },
  { label: "中", value: "medium" },
  { label: "大", value: "large" },
] as const;

as constconst 不是一回事

这两个写法长得有点像,但是作用不完全一样:

ts
const options = ["small", "medium", "large"];

这里的 const 只是说变量 options 不能重新赋值,但是数组内部仍然可以修改。

ts
const options = ["small", "medium", "large"] as const;

这里的 as const 会让 TypeScript 保留数组里的具体字面量,并且把数组推导为只读元组。

对比一下:

ts
const a = ["small", "medium", "large"];
// string[]

const b = ["small", "medium", "large"] as const;
// readonly ["small", "medium", "large"]

其他可以用到的地方

这个方法不只是适合单选组件。只要有“值只能从固定列表里选”的场景,都可以用:

  • 下拉框。
  • 标签筛选。
  • 排序方式。
  • 表格状态。
  • 权限名称。
  • 路由名称。

比如:

ts
const sortOptions = ["newest", "oldest", "popular"] as const;

type SortType = (typeof sortOptions)[number];

function changeSort(type: SortType) {
  console.log(type);
}

changeSort("newest"); // 正确
changeSort("random"); // 报错

我最后记住的写法

数组里是普通字符串时:

ts
const values = ["a", "b", "c"] as const;
type Value = (typeof values)[number];

数组里是对象时:

ts
const options = [
  { label: "选项 A", value: "a" },
  { label: "选项 B", value: "b" },
] as const;

type Value = (typeof options)[number]["value"];

写组件参数时,记得兼容只读数组:

ts
type RadioGroupProps<TOptions extends readonly RadioOption[]> = {
  options: TOptions;
  value: TOptions[number]["value"];
};

我当时缺的就是 as const 这一小段。加上以后,TypeScript 才真正知道数组里有哪些固定值,编辑器也才能给出准确提示。