You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 21:24:52