TypeScript游戏可扩展动词系统:单注册表类型安全实现问题
类型安全的可扩展TypeScript动词系统:封装动词定义并保留关联联合类型
问题概述
需要在TypeScript游戏中构建可扩展动词系统:
- 预定义动词(如
move、attack),每个动词包含前置条件与执行逻辑 - 用户可基于动词构建带参数的动作序列(如
move to field 1) - 要求类型安全且仅维护一个动词注册表
- 希望将每个动词的类型定义与业务逻辑封装在一起(后续将接入valibot Schema解析本地存储动作)
已实现一个无编译错误的版本,但动词类型与逻辑分散在ParamsByType和verbs中;尝试从动词注册表推导类型时,出现参数类型不匹配的编译错误。
无错误的初始实现(类型与逻辑分离)
type Verb<Params> = { precondition: (params: Params) => boolean; execute: (params: Params) => void; }; // 单独维护参数类型映射 type ParamsByType = { goTo: number; attack: string }; // 逐个定义动词,依赖ParamsByType的类型 const goTo: Verb<ParamsByType["goTo"]> = { precondition: (v) => v > 0, execute: (v) => { console.log(v); }, }; const attack: Verb<ParamsByType["attack"]> = { precondition: (s) => s.length > 0, execute: (s) => { console.log(s); }, }; // 构建动词注册表 const verbs: { [K in keyof ParamsByType]: Verb<ParamsByType[K]> } = { goTo, attack, } as const; // 定义动作类型(关联动词与参数) type Action<K extends keyof ParamsByType> = { [P in K]: { verbType: P; params: ParamsByType[P]; }; }[K]; // 执行动作 function executeAction<K extends keyof ParamsByType>(action: Action<K>) { if (!verbs[action.verbType].precondition(action.params)) { throw new Error("Precondition not met"); } verbs[action.verbType].execute(action.params); }
有编译错误的尝试(封装动词定义后)
尝试将动词类型与逻辑封装到单一定义中,从注册表推导参数类型时出现错误:
type Verb<Params> = { precondition: (params: Params) => boolean; execute: (params: Params) => void; }; // 直接定义动词,内置参数类型 const goTo: Verb<number> = { precondition: (v) => v > 0, execute: (v: number) => { console.log(v); }, }; const attack: Verb<string> = { precondition: (s) => s.length > 0, execute: (s: string) => { console.log(s); }, }; // 构建注册表 const verbs = { goTo, attack, } as const; type VerbMap = typeof verbs; // 从注册表推导参数类型映射 type ParamsByType = { [K in keyof VerbMap]: Parameters<VerbMap[K]["execute"]>[0]; }; // 定义动作类型 type Action<K extends keyof ParamsByType> = { [P in K]: { verbType: P; params: ParamsByType[P]; }; }[K]; // 执行动作:出现编译错误,参数类型被识别为联合类型无法匹配 function executeAction<K extends keyof ParamsByType>(action: Action<K>) { if (!verbs[action.verbType].precondition(action.params)) { // 错误:string | number 无法赋值给 never throw new Error("Precondition not met"); } verbs[action.verbType].execute(action.params); // 同样错误 } executeAction({ verbType: "goTo", params: 42 });
解决方案
问题根源:TypeScript无法自动关联泛型上下文中action.verbType与action.params的具体类型,导致参数被拓宽为联合类型。以下是两种可行方案:
方案1:使用判别式联合类型重构(推荐)
直接从动词注册表推导完整的动作联合类型,利用TypeScript的判别式特性自动关联动词与参数类型:
type Verb<Params> = { precondition: (params: Params) => boolean; execute: (params: Params) => void; }; // 封装动词的类型与逻辑 const goTo: Verb<number> = { precondition: (v) => v > 0, execute: (v) => console.log(`移动到区域:${v}`), }; const attack: Verb<string> = { precondition: (s) => s.length > 0, execute: (s) => console.log(`用${s}攻击`), }; // 动词注册表 const verbs = { goTo, attack } as const; type VerbMap = typeof verbs; // 从注册表推导动作联合类型(自动关联动词与参数) type Action = { [K in keyof VerbMap]: { verbType: K; params: Parameters<VerbMap[K]["execute"]>[0]; }; }[keyof VerbMap]; // 执行函数直接接收动作联合类型,TypeScript自动判别类型 function executeAction(action: Action) { const verb = verbs[action.verbType]; if (!verb.precondition(action.params)) { throw new Error("前置条件不满足"); } verb.execute(action.params); } // 类型安全的调用 executeAction({ verbType: "goTo", params: 42 }); executeAction({ verbType: "attack", params: "长剑" }); // 编译错误:参数类型不匹配 // executeAction({ verbType: "goTo", params: "invalid" });
方案2:保留泛型并使用类型断言
如果需要保留泛型函数,可以通过类型断言明确参数类型:
// 复用前面的Verb、verbs、VerbMap定义 type Action<K extends keyof VerbMap> = { verbType: K; params: Parameters<VerbMap[K]["execute"]>[0]; }; function executeAction<K extends keyof VerbMap>(action: Action<K>) { const verb = verbs[action.verbType]; // 用类型断言告诉TypeScript参数的具体类型 if (!verb.precondition(action.params as Parameters<typeof verb["precondition"]>[0])) { throw new Error("前置条件不满足"); } verb.execute(action.params as Parameters<typeof verb["execute"]>[0]); }
后续扩展(接入valibot Schema)
若需添加Schema解析本地存储动作,可直接扩展Verb类型,将Schema与动词定义封装在一起:
import { Schema, parse } from "valibot"; type Verb<Params> = { schema: Schema<Params>; // 添加valibot Schema precondition: (params: Params) => boolean; execute: (params: Params) => void; }; const goTo: Verb<number> = { schema: number([minValue(1)]), precondition: (v) => v > 0, execute: (v) => console.log(`移动到区域:${v}`), }; // 解析本地存储的原始动作 function parseStoredAction(rawAction: unknown): Action { const parsedBase = parse(object({ verbType: enum_(["goTo", "attack"]), params: unknown_, }), rawAction); const verb = verbs[parsedBase.verbType]; const parsedParams = parse(verb.schema, parsedBase.params); return { verbType: parsedBase.verbType, params: parsedParams }; }
内容的提问来源于stack exchange,提问作者thSoft
相关产品推荐
相关产品推荐

