Gleam 编译器 TypeScript 声明生成验证:typescript_declarations 集成测试解析
【免费下载链接】gleam⭐️ A friendly language for building type-safe, scalable systems!项目地址: https://gitcode.com/GitHub_Trending/gl/gleam
本指南以仓库中 typescript_declarations 集成测试项目 为主线,讲解 Gleam 语言在 JavaScript 目标下如何自动生成 TypeScript 类型声明(.d.mts文件),以及如何借助tsc(经由bunx)对这些声明进行编译级校验。读完本文,你将掌握 Gleam 项目开启 TypeScript 声明生成的配置方法、satisfies断言与isX()类型收窄重载的验证思路,以及该测试在仓库 CI 体系中的运行方式与底层实现位置。
一、这个测试项目要解决什么问题
test/typescript_declarations/README.md 的开篇只有一句话,却精准概括了它的使命:
Check that generated TypeScript declarations are correct. It uses
tscviabunx.
当 Gleam 代码被编译到 JavaScript 目标时,编译器除了产出.mjs运行时模块,还会根据配置生成配套的 TypeScript 声明文件(.d.mts),让 TypeScript 工程可以类型安全地消费 Gleam 模块。声明文件的正确性直接决定下游 TypeScript 使用体验:类型别名是否保留、常量类型是否准确、泛型参数是否落在正确位置、记录变体的收窄函数(isX())能否帮助类型系统正确推断——这些细节一旦出错,TypeScript 消费者就会得到错误的类型提示,甚至编译失败。
该测试项目正是为此而生:它编写了一个小而全的 Gleam 模块,编译后生成声明文件,再用 TypeScript 编译器tsc(通过bunx调用,无需全局安装)以严格模式检查这些声明是否符合预期。这是一种"用类型编译器验证类型编译器"的集成测试方案。
二、项目结构与各文件职责
仓库中该测试项目的完整结构如下(目录列表):
test/typescript_declarations/ ├── Makefile # 测试入口:构建 + tsc 校验 ├── README.md # 测试说明 ├── gleam.toml # 项目配置(开启声明生成) ├── manifest.toml # 依赖锁文件 ├── main.ts # 主声明校验脚本(satisfies 断言) ├── typescript_is_overload.ts # 类型收窄重载校验脚本 └── src/ ├── typescript_declarations.gleam # 被测试的 Gleam 模块 └── typescript_is_overload.gleam # 泛型自定义类型(Box)整个测试的流程是:Gleam 源码 →gleam build编译生成.d.mts声明 → 两个.ts脚本用tsc --strict对声明做类型断言校验。任何一环的类型信息不正确,tsc都会报错,测试即失败。
三、如何开启 TypeScript 声明生成
3.1 配置项:[javascript] typescript_declarations = true
测试项目的 gleam.toml 给出了最小可用配置:
name = "typescript_declarations" version = "1.0.0" target = "javascript" [javascript] typescript_declarations = true [dependencies] gleam_stdlib = ">= 0.44.0 and < 2.0.0"关键点有两个:
target = "javascript":声明生成仅作用于 JavaScript 编译目标(Erlang 目标不产出 TypeScript 声明);[javascript] typescript_declarations = true:显式开启声明文件生成开关。
在编译器源码中,该开关对应 compiler-core/src/config.rs 里的pub typescript_declarations: bool字段,并在package_config_to_json的快照(如 compiler-core/src/snapshots/gleam_core__config__package_config_to_json.snap)中序列化保存。从源码结构可以推断,该字段由gleam.toml的[javascript]段解析得到,随后传递给 JavaScript 代码生成阶段,决定是否渲染.d.mts文件。
3.2 依赖约束
manifest.toml 锁定gleam_stdlib为0.62.1(要求>= 0.44.0 and < 2.0.0)。测试模块function_option()中直接调用了gleam/option的option.Some(0),其声明(Option$)正是从build/dev/javascript/gleam_stdlib/gleam/option.d.mts导入的——这意味着测试同时校验了标准库生成的声明与被测项目自身生成的声明。
四、声明正确性如何被验证:satisfies断言
4.1 校验脚本 main.ts
main.ts 是声明校验的核心,其注释说明了策略:
// These statements use the `satisfies` keyword to assert the types are // what we expect.它先从构建产物导入类型与模块:
import type { Option$ } from "./build/dev/javascript/gleam_stdlib/gleam/option.d.mts"; import type { List } from "./build/dev/javascript/typescript_declarations/gleam.d.mts"; import * as gleam from "./build/dev/javascript/typescript_declarations/typescript_declarations.mjs";然后逐条对每个导出符号做类型断言。satisfies是 TypeScript 4.9+ 的关键字:它要求表达式的类型与给定类型兼容,但不会像类型注解那样收窄推断结果,非常适合"断言类型是预期值"的测试场景。例如:
gleam.const_int satisfies number gleam.const_int_alias satisfies number gleam.const_int_list satisfies List<number> gleam.const_string_list satisfies List<string> gleam.const_tuple satisfies [string, number] gleam.either_int satisfies gleam.Either$<number, number> gleam.function_int_int_returns_int_alias satisfies (a: number, b: number) => number gleam.function_closure_returns_fn_int_which_returns_int_alias satisfies () => (a: number) => number这些断言覆盖了声明生成器的各类核心场景:
| 被测 Gleam 符号 | 断言的 TS 类型 | 验证要点 |
|---|---|---|
const_int/const_int_alias | number | Int别名在声明中正确展开为number |
const_string_list/const_int_list | List<string>/List<number> | 泛型容器List的元素类型正确 |
const_tuple | [string, number] | 元组被映射为 TS 元组类型 |
either_int | Either$<number, number> | 泛型自定义类型Either(a, b)的类型参数按声明顺序填充 |
function_int_int_returns_int_alias | (a: number, b: number) => number | 函数别名保留完整函数签名 |
function_closure_returns_fn_int_which_returns_int_alias | () => (a: number) => number | 闭包返回的高阶函数签名正确 |
4.2 泛型函数的断言
对泛型函数function_generic_fn_generic_which_returns_generic_returns_generic,测试定义了具名泛型别名来断言:
type GenericFn<T = any> = (a: T, fn: (a: T) => T) => T; gleam.function_generic_fn_generic_which_returns_generic_returns_generic satisfies GenericFn对应 typescript_declarations.gleam 中的定义:
pub fn function_generic_fn_generic_which_returns_generic_returns_generic( val: a, function: fn(a) -> a, ) -> a { function(function(val)) }以及标准库 Option 的消费场景:
gleam.function_option satisfies () => Option$<number>对应 Gleam 侧function_option() -> Option(Int)返回option.Some(0)。这里连标准库gleam/option.d.mts的Option$<number>一并被验证。
4.3 被测 Gleam 模块
typescript_declarations.gleam 是一个"麻雀虽小五脏俱全"的样例模块,刻意覆盖了声明生成器的主要语法面:
- 类型别名:
pub type IntAlias = Int - 各类常量:
Int、IntAlias、List(String)、List(Int)、元组#("hello", 0) - 函数及其别名:
function_int_int_returns_int与pub const function_int_int_returns_int_alias = function_int_int_returns_int - 高阶函数与闭包:
function_closure_returns_fn_int_which_returns_int_alias - 泛型函数:
function_generic_fn_generic_which_returns_generic_returns_generic - 泛型自定义类型:
pub type Either(a, b) { Left(a) Right(b) } - 标准库类型消费:返回
Option(Int)
五、类型收窄重载:isX()的专项验证
5.1 为什么要单独测试isX()
Gleam 的自定义类型编译到 JavaScript 后,每个变体都会生成一个is<Variant>()判定函数,用于在运行时区分记录属于哪个变体。为了让 TypeScript 消费者在if (isX(value))分支内获得类型收窄(type narrowing)能力,声明生成器必须为这些函数生成带有泛型参数的重载签名(overload)。
第二个脚本 typescript_is_overload.ts 专门验证这一点。测试用了一个三类型参数的自定义类型:
// src/typescript_is_overload.gleam pub type Box(a, b, c) { AlmostFull(a, c) AlmostEmpty(b) Empty }三个变体分别持有不同位置、不同数量的类型参数:AlmostFull使用a、c,AlmostEmpty使用b,Empty不使用任何参数——这是对收窄逻辑最刁钻的考验。
5.2 断言逻辑:收窄前后的类型对比
const almost_empty = $box.Box$AlmostEmpty(1); almost_empty satisfies $box.Box$<unknown, number, unknown>; if ($box.Box$isAlmostEmpty(almost_empty)) { almost_empty satisfies $box.AlmostEmpty<number>; }要点解读:
- 构造器返回的是完整的
Box$<a, b, c>,其中未使用的类型参数位置被填充为unknown(如Box$<unknown, number, unknown>); - 一旦通过
Box$isAlmostEmpty(almost_empty)判定,tsc必须能把变量收窄为具体变体类型AlmostEmpty<number>,并且b参数被正确保留为number; - 三个断言组合起来,同时验证了
AlmostFull(Box$<string, unknown, number>→AlmostFull<string, number>)、AlmostEmpty和Empty(Box$<unknown, unknown, unknown>→Empty)三种收窄路径。
如果生成的isX()重载签名中泛型参数位置写错、类型参数个数不对或缺少收窄重载,if分支内的satisfies断言就会编译失败。
5.3 源码侧的实现对应
从 compiler-core/src/javascript/typescript.rs 的variant_check_definition函数可以看到,声明生成器对每个记录构造器(variant)会生成两类东西:
- 一个"宽泛"的判定签名:
export function Box$isAlmostEmpty(value: any): value is Box$<unknown, number, unknown>——未参与该变体的类型参数位置显式输出为unknown; - 当变体携带类型参数时,额外生成一个重载签名:
export function Box$isAlmostEmpty<a, b, c>(value: Box$<a, b, c>): value is AlmostEmpty<b>——判定通过后把值收窄为携带正确泛型实参的变体类型。
从源码结构看(第 748 行的overload分支、第 735~744 行对unknown占位符的拼接逻辑),重载签名会先于宽泛签名输出,从而让 TypeScript 在类型收窄时优先匹配精确的重载版本。这正好解释了测试脚本中"if分支内类型变为具体变体"的行为。
六、如何运行:Makefile 与 tsc 参数
6.1 测试命令
项目级 Makefile 定义了test目标:
.PHONY: test test: cargo run --quiet -- build bunx tsc ./main.ts --strict --noEmit --skipLibCheck false --lib es2020,dom bunx tsc ./typescript_is_overload.ts --strict --noEmit --skipLibCheck false --lib es2020,dom命令拆解:
cargo run --quiet -- build:调用当前仓库的 Gleam 编译器(cargo直接运行gleam-bin)执行build,把src/下的 Gleam 模块编译为build/dev/javascript/...下的.mjs与.d.mts;bunx tsc ...:通过bunx(Bun 自带的包执行器,等价于npx)临时拉取并运行 TypeScript 编译器,无需在项目里安装typescript依赖;./main.ts与./typescript_is_overload.ts分别代表两组独立的断言脚本。
6.2 各 tsc 参数的作用
| 参数 | 含义 | 在本测试中的意义 |
|---|---|---|
--strict | 开启全部严格类型检查 | 声明文件必须在最严格模式下通过,杜绝"恰好能用"的宽松声明 |
--noEmit | 只做类型检查,不输出 JS | 测试只关心类型正确性 |
--skipLibCheck false | 对.d.ts/.d.mts声明文件本身做检查 | 关键:让tsc直接检查 Gleam 生成的声明文件内部的类型一致性,而不只是消费方视角 |
--lib es2020,dom | 指定可用的标准库类型(ES2020 + DOM) | 为脚本提供Promise、console等环境类型,同时限定声明的兼容基线 |
6.3 在仓库整体测试中的位置
根目录 Makefile 提供了独立入口:
.PHONY: typescript-declarations-test typescript-declarations-test: ## Check that generated TypeScript declaration compile cd test/typescript_declarations && make test同时,根 Makefile 的test目标(第 27 行cd test/typescript_declarations && make test)已把该项目纳入全量集成测试,与test/language、test/javascript_prelude、test/project_javascript等项目并列执行。也就是说,任何改动只要影响了声明生成器的输出,运行根目录make test或单独的make typescript-declarations-test即可立刻发现回归。
6.4 本地复现步骤
在已安装 Rust 工具链与 Bun(提供bunx)的环境中:
# 方式一:直接进入测试目录 cd test/typescript_declarations make test # 方式二:通过仓库根 Makefile make typescript-declarations-testmake clean可清理build产物后重新构建(见 Makefile)。需要说明的是,仓库中的test/与build/目录为运行期产物,编译成功后自动生成,无需手工准备。
七、从测试反推的声明生成器设计要点
综合上述测试与源码,可以归纳出 Gleam TypeScript 声明生成器在设计上的几条可验证约束:
- 类型别名直接展开:
IntAlias在声明中解析为number,不保留多余的中间别名层(见 typescript_declarations.gleam 与 main.ts 的断言); - 元组、列表等内建类型映射为 TS 原生结构:元组 → 元组类型,
List→ 带泛型参数的List<T>声明(从gleam.d.mts导入List可见); - 自定义类型的变体收窄依赖重载:
isX()通过"重载 +unknown占位"双签名实现精确收窄(typescript.rs); - 字段访问符生成位置索引函数:
variant_fields_definition(typescript.rs 起)为每个变体字段生成Type$Variant$Index形式的取值函数,保证添加标签字段不破坏既有访问方式; - 标准库与项目声明同时被校验:测试脚本同时 import 了
gleam_stdlib与项目自身的.d.mts,声明生成器对任意 Gleam 包一视同仁。
八、总结
typescript_declarations 项目虽小,却是 Gleam JavaScript 目标类型安全承诺的关键一环:它用tsc --strict --skipLibCheck false双重严格模式,把"生成的 TypeScript 声明必须正确"从口头约定变成了可自动执行的集成测试。无论是常量、类型别名、泛型自定义类型,还是isX()的类型收窄重载,每一个声明细节都被satisfies断言钉死。对希望在自己的 Gleam + TypeScript 混合工程中开启[javascript] typescript_declarations = true的开发者而言,这个测试项目既是最好的配置范例,也是理解声明生成器行为边界的活文档。
【免费下载链接】gleam⭐️ A friendly language for building type-safe, scalable systems!项目地址: https://gitcode.com/GitHub_Trending/gl/gleam
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考