TypeScript:从后端语言切换到客户端
级别:首作必学。 学完应能写出带类型的规则函数,理解 JavaScript 实际执行什么,并能独立读取引擎组件脚本。
先区分类型系统与运行时
TypeScript 在开发阶段检查类型,输出 JavaScript 后,大部分类型信息不再存在。接口声明不是数据校验器,客户端存档、服务器响应和配置都需要在入口验证。你在后端熟悉的 DTO 校验责任仍然存在。
type Level = { id: string; pairs: number; turns: number };
function isLevel(value: unknown): value is Level {
if (typeof value !== 'object' || value === null) return false;
const v = value as Record<string, unknown>;
return typeof v.id === 'string' &&
Number.isInteger(v.pairs) && Number.isInteger(v.turns) &&
(v.pairs as number) > 0 && (v.turns as number) > 0;
}
这是字段层面的最小检查;棋盘容量、关卡引用、资源是否存在仍需业务验证。避免把 JSON.parse(raw) as Level 当成验证。
首先掌握的语法
| 内容 | 游戏中的使用 |
|---|---|
const、let、数组与对象 | 表达关卡、牌和局内状态 |
| 联合类型、类型收窄 | 明确 playing、resolving、won 等分支 |
| 函数、闭包、箭头函数 | 规则转换、输入回调、保留监听函数 |
class、this | 阅读 Cocos 的组件实例 |
import、export | 规则、视图与平台代码分文件 |
Promise、async/await | 资源加载与网络请求 |
null 与 undefined | 未绑定属性、缺失存档和可选字段 |
const 保证变量不能重新赋值,不保证对象不可变。数组 sort、splice 等会修改原数组;规则函数若承诺返回新状态,应主动复制。浮点数也不适合直接承担可信货币金额,资源数量优先使用有上界的整数。
一组可以立刻读懂的基础写法
// 用对象表达配置,用函数表达规则;类型不会改变运行时数据。
export type Config = {pairs: number; turns: number};
export function cardCount(config: Config): number {
return config.pairs * 2;
}
const level: Config = {pairs: 3, turns: 6};
const ids = Array.from({length: cardCount(level)}, (_, id) => id);
const evenIds = ids.filter(id => id % 2 === 0);
const labels = evenIds.map(id => `第 ${id + 1} 张`);
map 把每项转换成另一项,filter 保留满足条件的项;箭头函数可捕获外部变量。数组和对象赋值通常共享引用,{...level} 只做浅复制,嵌套对象仍可能共享。
比较通常使用 ===,避免隐式类型转换;缺失值可通过 null、undefined 和可选链 obj?.field 处理。value ?? defaultValue 只在空值时回退,不会把合法的 0 或 false 当成缺失。JSON 里不支持 undefined,序列化边界要主动约定。
import {cardCount} from './rules' 对应另一个模块的 export;类型可以单独导出,不会成为运行时对象。不要假定浏览器或微信拥有 Node 的文件系统和任意 npm 包能力。先确认依赖是否适合当前运行环境。
用联合类型表达游戏阶段
type Phase = 'playing' | 'resolving' | 'won' | 'lost';
function canFlip(phase: Phase): boolean {
return phase === 'playing';
}
比起同时维护 isGameOver、isWin、isLocked 三个布尔值,一个明确阶段更容易避免不合法组合。复杂系统可拆分局内阶段与应用暂停状态,见状态设计。
this 与监听函数
把类方法直接传给第三方回调时,不能假定 this 仍指向组件。使用引擎支持的目标参数,或保存一个箭头函数引用。注册与解绑必须使用同一个引用,临时再次调用 bind 会得到另一个函数。
异步任务与错误
async function loadConfig(read: () => Promise<unknown>): Promise<Level> {
const value = await read(); // 让出执行权,其他输入仍可能发生
if (!isLevel(value)) throw new Error('关卡配置格式错误');
return value;
}
// 调用方需要 try/catch;错误必须进入界面的失败或重试状态。
Promise 表达以后成功或失败的结果,await 不会冻结整个游戏。加载完成时当前场景可能已经变化,归属检查和取消策略见运行时。
版本与类型检查
本站规则示例使用独立 TypeScript 检查;Cocos 有自己的编译链与装饰器约定,不能简单把本站 tsconfig 拷进 Creator。引擎构建成功也不等于所有类型错误都已经检查。以固定 Creator 版本的项目配置为准,并保留编辑器类型检查步骤。
集合与迭代器也要检查编译后的行为。本站浏览器验证曾发现数组展开的宽松编译假设会影响 Map.values(),规则源因此使用 Array.from(counts.values())。同一份 TS 在 Node 检查通过,并不能替代目标构建链的实际运行。
练习:给一份读取到的关卡 JSON 加上运行时验证,分别输入字符串回合数、负数牌对和缺失 ID。完成标准:错误出现在配置入口,并能指出具体字段,而不是到翻牌时才崩溃。