如何在TypeScript中实现仅类型上下文可见的透明可推导嵌套类型
你需要实现的需求对应的标准技术术语是幽灵类型(Phantom Type),也叫类型级元数据,核心就是在类型层面存储关联信息,不会影响实际的运行时结构和智能提示。下面给出两个可直接落地的优化方案:
方案1:私有Unique Symbol存储类型元数据(改动最小)
TypeScript的智能提示默认不会展示unique symbol类型的键属性,只要你不导出这个symbol,外部代码完全感知不到它的存在,也不会出现在补全列表里,和你现有代码的适配成本最低。
实现代码:
/** 私有Symbol,仅用于类型层面存储结果引用,不导出的话外部完全无法访问 */ declare const __ActionResultType: unique symbol; /** * Represents a result of an action */ type ActionResult< ActionName extends string = string, ResultProps = {} > = { type: ActionName, result: true, } & ResultProps; /** * Represents an Action, but also contains within it the type of it's result */ type Action< ActionName extends string = string, Props = {}, ResultProps = {} > = { type: ActionName, result?: undefined, // 用私有Symbol作为键,不会出现在智能提示里 [__ActionResultType]?: ActionResult<ActionName, ResultProps>, } & Props; /** * Type guard for a specific action */ function isAction<A extends Action>(actionName: A['type'], action: Record<string, unknown>): action is A { return action.type === actionName && action.result === undefined; } /** * Derive the Result type from an Action type */ type ResultFromAction< A extends Action > = NonNullable<A[typeof __ActionResultType]>; /** * Type guard for a specific action result (by Action) */ function isActionResult<A extends Action>(actionName: A['type'], result: Record<string, unknown>): result is ResultFromAction<A> { return result.type === actionName && result.result; }
优势:
- 完全兼容你现有的代码逻辑,仅需修改Action的属性键
- 智能提示不会出现任何额外属性,外部代码无法访问该私有Symbol
- 不需要额外的注册步骤,使用方式和你之前完全一致
方案2:全局类型映射表(完全无额外属性)
如果你希望Action类型上完全不存在任何额外的属性(哪怕是Symbol键),可以用类型映射表+声明合并的方案,把所有关联关系存在全局的类型接口里,结构100%干净。
实现代码:
/** * Represents a result of an action */ type ActionResult< ActionName extends string = string, ResultProps = {} > = { type: ActionName, result: true, } & ResultProps; /** 全局类型映射表,用来存储Action名和对应结果属性的映射关系 */ interface ActionResultRegistry {} /** * Represents an Action */ type Action< ActionName extends keyof ActionResultRegistry, Props = {} > = { type: ActionName, result?: undefined, } & Props; /** * Type guard for a specific action */ function isAction<A extends Action<keyof ActionResultRegistry>>(actionName: A['type'], action: Record<string, unknown>): action is A { return action.type === actionName && action.result === undefined; } /** * Derive the Result type from an Action type */ type ResultFromAction< A extends Action<keyof ActionResultRegistry> > = ActionResult<A['type'], ActionResultRegistry[A['type']]>; /** * Type guard for a specific action result (by Action) */ function isActionResult<A extends Action<keyof ActionResultRegistry>>(actionName: A['type'], result: Record<string, unknown>): result is ResultFromAction<A> { return result.type === actionName && result.result; }
使用方式:
每次定义新的Action之前,先扩展全局的映射表即可:
// 先注册所有Action的结果类型 interface ActionResultRegistry { navigate: { success: boolean }, incrementCounter: { newValue: number } } // 再定义Action类型 type NavigateAction = Action<'navigate', { path: string }>; type IncrementCounterAction = Action<'incrementCounter', { valueToAdd: number }>;
优势:
- Action类型完全没有任何多余属性,结构干净
- 所有Action的结果类型统一管理,便于维护
内容的提问来源于stack exchange,提问作者Frank Weindel
相关产品推荐
相关产品推荐

