02 typeof、keyof 与索引访问:从配置派生类型
核心目标:既保存一份运行时可用的配置,又让类型跟随这份配置变化。
1. 同一个 typeof,两个使用位置
const statuses = ["pending", "active", "banned"] as const;
console.log(typeof statuses); // JavaScript 表达式:运行时输出 "object"
type Statuses = typeof statuses; // TS 类型查询:得到这个变量的静态类型
第一句计算值;第二句定义类型,编译后消失。判断关键不是拼写,而是它位于“值表达式”还是“类型位置”。
JS 的 typeof 不会输出 TS 联合类型;TS 的 typeof 也不会在运行时检查数组。
TS 类型查询通常引用变量或属性,而不是任意调用表达式。需要函数返回类型时用 ReturnType,见 09 章。
2. 逐步拆开你的写法
const statuses = ["pending", "active", "banned"] as const;
type Status = (typeof statuses)[number];
第一步:从值拿到类型。
type Statuses = typeof statuses;
// readonly ["pending", "active", "banned"]
第二步:通过数字索引查询元素类型。
type FirstStatus = Statuses[0]; // "pending"
type SecondStatus = Statuses[1]; // "active"
type AnyStatus = Statuses[number]; // "pending" | "active" | "banned"
这里 number 是类型,不是名为 number 的变量,也不是“取第 number 项”。它表示所有数字索引可能对应的元素类型。
合起来就是:
typeof statuses:数组/元组类型
↓
[number]:取其所有元素类型
↓
"pending" | "active" | "banned"
这个定义里没有执行数组遍历。联合类型是编译期表达,运行时仍需要 statuses 才能做 includes 等校验。
3. 不使用 as const 会怎样?
const statuses = ["pending", "active", "banned"];
type Status = (typeof statuses)[number]; // string
typeof 拿到的是 string[],元素类型自然是 string。[number] 不能恢复已经被扩大丢掉的具体值信息。
这也是“先构造精确配置,再派生类型”比事后大量断言更清楚的原因。
4. keyof:拿到属性名,不是属性值
interface CustomerSummary {
customer_id: number;
first_name: string;
active: boolean;
}
type CustomerKey = keyof CustomerSummary;
// "customer_id" | "first_name" | "active"
type CustomerId = CustomerSummary["customer_id"]; // number
type NameOrActive = CustomerSummary["first_name" | "active"];
// string | boolean
keyof 是类型操作符,不能写 console.log(keyof CustomerSummary)。interface 也不能直接放进 Object.keys,它不是运行时对象。
带索引签名的类型,其 keyof 可能是 string/number 等宽范围;不要认为所有 keyof 都是一串有限字符串。
5. keyof typeof 配置对象
const sort_columns = {
id: "customer_id",
name: "last_name",
created: "create_date",
} as const;
type SortKey = keyof typeof sort_columns;
// "id" | "name" | "created"
type SortColumn = (typeof sort_columns)[SortKey];
// "customer_id" | "last_name" | "create_date"
客户传的是 API 排序选项 id/name/created,内部映射成数据库列名;可以不把数据库列名直接暴露为请求契约。
function getSortColumn(key: SortKey): SortColumn {
return sort_columns[key];
}
这只是新的教学映射,不要求修改当前项目使用 customer_id/last_name/create_date 的排序接口。
6. 动态字符串不能随意索引固定对象
function getSortColumn(input: string) {
return sort_columns[input]; // 错误:任意 string 不保证是合法键
}
不要马上把对象写成 Record<string, string>;这会扩大键范围,反而丢掉固定配置的限制。
在输入边界做真实检查:
function isSortKey(value: unknown): value is SortKey {
return typeof value === "string"
&& Object.prototype.hasOwnProperty.call(sort_columns, value);
}
function getSortColumnFromInput(input: unknown): SortColumn {
if (!isSortKey(input)) throw new Error("不支持的排序选项");
return sort_columns[input];
}
这个类型守卫依赖 sort_columns 是由程序控制、键集合稳定的对象。运行时校验与类型收窄分别完成自己的工作。
如果用 input in sort_columns,还要知道 in 会查看原型链;固定白名单检查自身键更明确。
7. 为什么不能从“动态数据”推导固定联合?
const statuses_from_db: string[] = ["active", "pending"];
type Status = (typeof statuses_from_db)[number]; // string
TS 检查时数据库还没返回数据。编译期固定配置能推导字面量联合,运行时获取的任意字符串数组通常不能。
也不能对任意变量 statuses_from_db as const,让 TS 把未来的数据库内容变成固定元组。as const 有适用表达式限制,且没有读取数据库的能力。
动态数据要用运行时验证、业务定义或代码生成流程,不是把断言写得更复杂。
8. 这能减少哪些重复?
export const account_statuses = ["pending", "active", "banned"] as const;
export type AccountStatus = (typeof account_statuses)[number];
同一份固定值集合可用于类型、验证白名单、状态选项。但前端和后端是否真的共享该模块,还取决于项目打包与契约设计,不会因为 export 就自动同步部署。
反过来,若只有一个类型别名 type AccountStatus = ...,运行时没法直接枚举它。应根据“是否需要一份值配置”决定方向,不是一切都必须数组驱动。
9. 索引访问类型与真正访问值的区别
type Element = (typeof account_statuses)[number]; // 类型定义
const first = account_statuses[0]; // 运行时取值
T[number] 查询元素类型,不表示“任意下标一定存在”。普通数组运行时越界仍会得到 undefined;项目开启的 noUncheckedIndexedAccess 会在实际元素访问中提醒这一点。
不要把“元素类型不含 undefined”误读成“任意索引都不会越界”。
10. 练习
- 从允许的排序列数组派生 SortColumn,不再手写联合。
- 从状态到中文文案的对象派生键类型,解释键联合与值联合。
- 去掉 as const,观察派生的值类型是否扩大。
- 写一个接收 unknown 的排序检查函数,拒绝不支持的字符串。
下一章:标注、as 与 satisfies。参考:Typeof、Indexed Access。