目标:让 TypeScript 实现与 Rust Guest 完全相同的 WIT world,并由同一个 Wasmtime Host 调用;同时解决一个常见疑问------"为什么 TS 不像 Rust 一样,直接引入 WIT 就自动有接口实现?"
答案先说在前面:Rust 的 wit-bindgen 可以生成待实现 trait;TypeScript Guest 的运行时边界却是 ESM module export。Jco 可以根据 WIT 生成 .d.ts,但不会替你写业务实现。正确做法是:生成声明 + satisfies 检查运行时 export 形状 + ComponentizeJS 封装。
1. TypeScript 与 Rust 绑定模型的差异
Rust 有 trait 作为语言级"待实现接口"。wit-bindgen 生成 trait 后,缺方法就不能编译。
TypeScript 的类型会在编译后擦除;ComponentizeJS 真正加载的是 ESM JavaScript。因此它必须看到这样的运行时值:
ts
export const api = {
greet(name: string) {
return `Hello, ${name}!`;
},
};
仅生成一个 interface 并不能在运行时创建 api 对象。反过来,只导出对象又缺少 WIT 约束。我们把两者组合起来。
2. TS Guest 的目录与 package scripts
text
guests/typescript/
├── src/guest.ts
├── generated/wit/ # 生成,不提交
├── dist/ # 构建,不提交
├── package.json
└── tsconfig.json
package.json 将包内流程明确写出来:
json
{
"scripts": {
"gen:wit": "jco guest-types ../../wit -o generated/wit --world-name plugin --strict",
"check": "tsc -p tsconfig.json",
"build:component": "jco componentize dist/guest.js --wit ../../wit --world-name plugin --out dist/wasm-component-lab-ts-guest.wasm --disable=all",
"build": "pnpm run gen:wit && pnpm run check && pnpm run build:component"
}
}
这里将 TypeScript 包内的类型生成、检查和组件封装定义为 package.json scripts,因此可直接执行 pnpm run build。示例中的 xtask 仅调用该命令,并在需要时继续执行 Host 验证。
3. 从 WIT 生成 Guest 类型
根目录 WIT:
wit
package wasm-component-lab:greeter@0.1.0;
interface api {
greet: func(name: string) -> string;
}
world plugin { export api; }
执行 pnpm run gen:wit 后,Jco 生成近似下面的声明:
ts
declare module "wasm-component-lab:greeter/plugin@0.1.0" {
export * as api from "wasm-component-lab:greeter/api@0.1.0";
}
declare module "wasm-component-lab:greeter/api@0.1.0" {
export function greet(name: string): string;
}
生成文件只服务 TypeScript typecheck;它不是 Component 本身,也不应该手工维护。tsconfig.json 必须将 generated/wit/**/*.d.ts 包含进编译范围。
4. 用 satisfies 把 WIT 绑定到真实导出
ts
const guestExports = {
api: {
greet(name: string): string {
return `Hello, ${name}! (from a TypeScript Wasm Component)`;
},
},
} satisfies typeof import("wasm-component-lab:greeter/plugin@0.1.0");
export const api = guestExports.api;
这里有一个很容易遗漏的细节:
satisfies在编译期检查 world 的整体形状,包括api名、函数参数与返回值;export const api在运行期提供 ComponentizeJS 所需的 ESM export;- 不能只写
as SomeType,断言会掩盖错误;也不能只写type Api = ...,它不会约束运行时对象。
现在把 WIT 改为 greet: func(name: string, locale: string) -> string,pnpm run check 就会立即指出实现漏了参数。这就是 TS 侧防止 WIT 接口漂移的关键。
5. 构建和验证命令
从仓库根目录运行:
powershell
cargo xtask build-ts
cargo xtask smoke-ts
本文使用 xtask 提供两个简化命令:build-ts 在 guests/typescript 中执行 pnpm run build;smoke-ts 在构建完成后运行根目录 Host:
该过程对应的实际步骤是:生成 WIT TypeScript 声明、执行 tsc、使用 ComponentizeJS 封装组件,以及由 Host 实例化并调用该组件。
6. TypeScript Guest 的业务边界与 I/O 模型
适合放进 TS Guest 的内容包括:规则计算、字段转换、文本处理、策略编排,以及可以由 ComponentizeJS 支持的 JavaScript 逻辑。
不应直接传入 Guest 的对象包括文件句柄、进程全局配置和完整应用状态;它们属于 Host 的运行时上下文。
例如,未来将接口扩展为 format(text) 后,Host 可以先读取用户指定的文本文件和配置,再将文本处理任务传给同步的 Guest 函数;或者通过 WIT import 提供受控的 configuration.get。这比让组件直接访问本地 I/O 更容易测试、审计和替换语言实现。
需要特别注意:JavaScript 的 async 不等于 WIT 的组件级异步。当前 ComponentizeJS 对 export 的异步有特定的同步化支持,但 Guest import 并不等价于任意可 await 的 Host 异步函数。涉及文件读取、网络请求等 I/O 时,先采用 Host 预取 + 明确任务数据的方式最稳妥;WASI Preview 3/组件异步适合后续独立实验。
7. 验证与常见问题
powershell
cargo xtask smoke-ts
预期结果:
text
host: Hello, WIT learner! (from a TypeScript Wasm Component)
| 现象 | 原因与处理 |
|---|---|
TS 找不到 wasm-component-lab:... module |
先运行 pnpm run gen:wit,检查 tsconfig include |
componentize 找不到 export |
ESM 必须真的 export const api,不能只有类型 |
| TS 通过,Host 实例化失败 | 检查 WIT package、world、版本和封装命令;运行 smoke test |
想在 Guest await 文件或网络 I/O |
将 I/O 放在 Host,向 Guest 提供任务数据或受限 capability |
| 生成文件被手改后又丢失 | 正常现象;修 WIT 或业务代码,别维护 generated 文件 |