TS 数据驱动类型构造 export type Type = typeof DATA[keyof typeof DATA];
因为 TypeScript 类型信息默认不会保留到运行时,所以现代 TS 更倾向于让运行时数据作为唯一来源,再由数据推导类型,而不是先定义类型,再额外维护一份运行时类型元数据。而后端类型优先则需要把类型反射为数据。
核心是既要支持静态类型检查,也要类型元数据可以被传递。
现代前端更倾向于“运行时数据驱动 + 编译期类型增强”
以 JavaScript 数据作为事实来源(data as source of truth),TypeScript 类型从数据中推导出来。
ts 的类型转为 js 会消解,所以直接在js建类型数据,ts从中生成。则类型信息可以保留。这样同一份数据既保留了运行时能力,又提供了编译期类型信息。
在传统强类型语言中,我们通常习惯:
先定义类型,再根据类型创建数据。
例如 Java、C# 中:
Type / Class
|
↓
Runtime Object
类型是程序的核心模型,运行时对象根据类型产生。
但是在现代前端 TypeScript 开发中,流行另一种模式:
运行时数据驱动 + 编译期类型增强
即:
以 JavaScript 数据作为事实来源(Data as Source of Truth),TypeScript 类型从数据中推导出来。
Runtime Data
|
+----------------+
| |
↓ ↓
JavaScript TypeScript
运行时使用 类型检查
这也是为什么现代 React、Vue、Zustand、TanStack 等生态大量使用:
as const
typeof
keyof
组合模式。
一、传统类型优先的问题
假设定义一个状态:
type Status =
| "idle"
| "ready"
| "loading";
这个类型只能用于编译期检查。
例如:
function updateStatus(status: Status) {
}
可以:
updateStatus("idle");
但是:
Status
运行时不存在。
因为 TypeScript 类型会在编译后消失:
type Status =
| "idle"
| "ready"
| "loading";
编译:
// 什么都没有
所以无法:
Object.values(Status);
也无法:
for (const status of Status) {}
静态类型校验的类型信息在编译后被消解,元信息需要单独维护表,容易造成不一致。
二、如果需要运行时数据怎么办?
例如前端需要:
- 渲染下拉列表
- 状态校验
- 生成菜单
- 显示标签
- 判断接口返回值
需要:
const statuses = [
"idle",
"ready",
"loading"
];
于是出现:
Status 类型
|
| idle
| ready
| loading
statuses 数据
|
| idle
| ready
| loading
两个来源。
问题:
如果新增:
"error"
可能只修改其中一个。
类型:
type Status =
| "idle"
| "ready"
| "loading"
| "error";
但是数据:
const statuses = [
"idle",
"ready",
"loading"
];
运行时缺少。
三、现代前端的数据驱动方式
现代 TypeScript 更倾向:
不从类型生成数据,而是从数据生成类型。
例如:
export const STATUS = {
IDLE: "idle",
READY: "ready",
LOADING: "loading",
ERROR: "error",
} as const;
这里:
STATUS 是真实 JavaScript 对象。
运行时存在:
STATUS.IDLE
结果:
"idle"
可以:
Object.values(STATUS);
得到:
[
"idle",
"ready",
"loading",
"error"
]
四、关键:由值生成类型
核心代码:
export type Status =
(typeof STATUS)[keyof typeof STATUS];
这是现代 TypeScript 中非常重要的模式。
拆开理解。
1. typeof STATUS
注意这里不是 JavaScript 的:
typeof STATUS
而是 TypeScript 类型位置中的 typeof。
得到:
{
readonly IDLE: "idle";
readonly READY: "ready";
readonly LOADING: "loading";
readonly ERROR: "error";
}
2. keyof typeof STATUS
获取所有属性名:
"IDLE"
|
"READY"
|
"LOADING"
|
"ERROR"
也就是:
keyof typeof STATUS
结果:
"IDLE" | "READY" | "LOADING" | "ERROR"
3. 索引访问类型
继续:
(typeof STATUS)[keyof typeof STATUS]
等价于:
typeof STATUS[
"IDLE"
|
"READY"
|
"LOADING"
|
"ERROR"
]
TypeScript 会自动展开:
typeof STATUS["IDLE"]
|
typeof STATUS["READY"]
|
typeof STATUS["LOADING"]
|
typeof STATUS["ERROR"]
得到:
"idle"
|
"ready"
|
"loading"
|
"error"
最终:
export type Status =
"idle"
| "ready"
| "loading"
| "error";
五、as const 的作用
为什么需要:
as const
?
普通:
const STATUS = {
IDLE: "idle"
};
TypeScript 推断:
{
IDLE: string;
}
也就是:
任何字符串都可以
但是:
const STATUS = {
IDLE: "idle"
} as const;
推断:
{
readonly IDLE: "idle";
}
保留字面量类型。
因此:
typeof STATUS[keyof typeof STATUS]
才能得到:
"idle"
而不是:
string
六、类型优先 vs 数据驱动
类型优先
流程:
Type
|
↓
Runtime Data
需要:
- 反射
- 代码生成
- Schema
- 元数据
例如:
Java:
Class
|
↓
Reflection
|
↓
Runtime Metadata
类型本身可以被读取。
数据驱动
TypeScript 更常见:
Data
|
+---- Runtime
|
+---- Type
例如:
const STATUS = {
IDLE: "idle",
READY: "ready"
} as const;
export type Status =
(typeof STATUS)[keyof typeof STATUS];
一份数据:
同时提供:
运行时:
STATUS.IDLE
Object.values(STATUS)
编译期:
Status
七、完整实践
例如状态管理:
export const STATUS = {
IDLE: "idle",
READY: "ready",
LOADING: "loading",
ERROR: "error",
} as const;
export type Status =
(typeof STATUS)[keyof typeof STATUS];
const STATUS_LABELS: Record<Status, string> = {
[STATUS.IDLE]: "Idle",
[STATUS.READY]: "Ready",
[STATUS.LOADING]: "Loading",
[STATUS.ERROR]: "Error",
};
这里:
STATUS
|
+--> JavaScript运行时数据
|
+--> Status类型
Status
|
+--> 约束 STATUS_LABELS
总结
- JavaScript 数据天然存在于运行时
- TypeScript 类型只存在于编译期(附加推导校验)
- TS是JS数据驱动的类型校验
因此:
const DATA = {...} as const;
export type Type =
typeof DATA[keyof typeof DATA];
一段话
要获取元数据,在JS比较特殊。
反射是由强类型导出,而JS原生无强类型,由数据驱动。
所以就需要将类型信息直接作为数据,由TS导出为类型提供校验。
如此单一数据源,运行期有类型信息,编译期也有类型供校验

浙公网安备 33010602011771号