一个可序列化为 JSON 的表达式 DSL 库,提供类型安全的表达式构建、编译和求值。
- 🎯 类型安全 - 使用 TypeScript 泛型进行完整的编译时类型推导
- 📦 可序列化 - 编译后的表达式为纯 JSON 格式,易于传输和存储
- 🔧 灵活的表达式 - 支持任意 JavaScript 表达式,包括函数调用和对象属性访问
- ⚡ 高性能 - 优化的表达式编译和执行
- 🧩 可组合 - 表达式可以相互组合,形成复杂的计算树
- 🔄 短路求值 - 支持
&&、||、??和三元表达式的控制流优化 - 📝 内联优化 - 自动内联只被引用一次的子表达式
- 🔗 Proxy 变量系统 - 支持链式属性访问和方法调用,如
config.timeout、user.profile.name - 🎛️ Lambda 表达式 - 类型安全的数组方法支持(map、filter、reduce 等)
- 🛡️ 错误检测 - 编译时检测未定义变量和类型错误
bun install @codehz/json-exprimport{variable,expr,compile,evaluate,t,lambda,wrap}from"@codehz/json-expr";// 定义类型化变量(使用 TypeScript 泛型)constx=variable<number>();consty=variable<number>();// 构建表达式constsum=expr({ x, y })("x + y");constproduct=expr({ x, y })("x * y");constresult=expr({ sum, product })("sum + product");// 编译表达式(可序列化为 JSON)constcompiled=compile(result,{ x, y });// => [["x", "y"], "$[0]+$[1]", "$[0]*$[1]", "$[2]+$[3]"]// 执行编译后的表达式constvalue=evaluate(compiled,{x: 2,y: 3});// => 11 (2+3 + 2*3 = 5 + 6 = 11)// 使用模板字符串constname=variable<string>();constgreeting=t`Hello, ${name}!`;// 使用 lambda 表达式constnumbers=variable<number[]>();constdoubled=numbers.map(lambda<[number],number>((n)=>expr({ n })("n * 2")));// 使用 wrap 包装静态值constpattern=wrap(/^[a-z]+$/i);constinput=variable<string>();constisValid=pattern.test(input);变量是表达式中的占位符,使用 TypeScript 泛型定义其类型。
constage=variable<number>();constname=variable<string>();constconfig=variable<{debug: boolean;timeout: number;}>();表达式对变量或其他表达式进行运算,使用字符串形式描述。
constx=variable<number>();consty=variable<number>();// 简单表达式constsum=expr({ x, y })("x + y");// 复杂表达式(可以使用 JS 语言特性)constabs=expr({ x })("Math.abs(x)");constconditional=expr({ x, y })("x > y ? x : y");constarray=expr({ x, y })("[x, y].filter(v => v > 0)");编译后的表达式为 JSON 数组格式:
// 格式: [variableNames, expression1, expression2, ...]// 其中 $N 用于引用之前的变量或表达式constcompiled=compile(result,{ x, y });// [["x", "y"], "$[0]+$[1]", "$[0]*$[1]", "$[2]+$[3]"]创建一个类型化变量。
参数: 无(类型通过泛型参数 T 指定)
返回值: Variable 对象,支持属性访问和方法调用
示例:
constnum=variable<number>();conststr=variable<string>();constconfig=variable<{timeout: number}>();// 支持链式属性访问consttimeout=config.timeout;// 自动转换为表达式创建表达式,采用柯里化设计以支持完整的类型推导。
参数:
context- 上下文对象,包含变量和/或其他表达式的映射
返回值: 函数,接收表达式源码字符串并返回 Expression 对象
示例:
constx=variable<number>();consty=variable<number>();constsum=expr({ x, y })("x + y");constresult=expr({ sum, x })("sum * x");compile<TResult>(expression: Expression<any, TResult>, variables: Record<string, Variable<any>> | Variable<any>[], options?: CompileOptions): CompiledData
将表达式树编译为可序列化的 JSON 结构。
参数:
expression- 要编译的表达式variables- 表达式中使用的所有变量映射或数组options- 编译选项(可选)inline?: boolean- 是否启用内联优化,将只被引用一次的子表达式内联到使用位置(默认:true)
返回值: CompiledData 数组
示例:
constx=variable<number>();consty=variable<number>();constsum=expr({ x, y })("x + y");constproduct=expr({ x, y })("x * y");constresult=expr({ sum, product })("sum + product");constcompiled=compile(result,{ x, y });// [["x", "y"], "$[0]+$[1]", "$[0]*$[1]", "$[2]+$[3]"]// 禁用内联优化constnoInline=compile(result,{ x, y },{inline: false});// [["x", "y"], "$[0]+$[1]", "$[0]*$[1]", "$[2]+$[3]"]执行编译后的表达式。
参数:
data- 编译后的表达式数据values- 变量值映射,对应编译数据中的变量名顺序
返回值: 表达式计算结果
示例:
constx=variable<number>();consty=variable<number>();constsum=expr({ x, y })("x + y");constproduct=expr({ x, y })("x * y");constresult=expr({ sum, product })("sum + product");constcompiled=compile(result,{ x, y });constvalue=evaluate(compiled,{x: 5,y: 3});// => 23 ((5+3) + (5*3) = 8 + 15 = 23)使用标签模板函数创建包含变量的字符串表达式。
参数:
strings- 模板字符串的静态部分values- 模板中插值的变量和表达式
返回值: 字符串类型的 Proxy Expression
示例:
constname=variable<string>();constcount=variable<number>();constgreeting=t`Hello, ${name}!`;constmessage=t`You have ${count} items.`;constcompiled=compile(greeting,{ name });constresult=evaluate(compiled,{name: "Alice"});// => "Hello, Alice!"创建类型安全的 lambda 表达式,用于数组方法(map、filter、reduce 等)。
参数:
builder- Lambda 构建函数,接收参数代理,返回函数体表达式
返回值: Lambda 表达式,可在数组方法中使用
示例:
import{lambda}from"@codehz/json-expr";// 单参数 lambdaconstnumbers=variable<number[]>();constdoubled=numbers.map(lambda<[number],number>((n)=>expr({ n })("n * 2")));constcompiled=compile(doubled,{ numbers });constresult=evaluate(compiled,{numbers: [1,2,3]});// => [2, 4, 6]// 多参数 lambda(reduce)constsum=numbers.reduce(lambda<[number,number],number>((acc,val)=>expr({ acc, val })("acc + val")),0);// 捕获外部变量constmultiplier=variable<number>();constscaled=numbers.map(lambda<[number],number>((n)=>expr({ n, multiplier })("n * multiplier")));将静态值包装为 Proxy Expression,使其可以像 Variable 一样调用方法和访问属性。
参数:
value- 要包装的静态值(支持原始值、对象、数组、Date、RegExp、BigInt、URL、Map、Set、TypedArray 等)
返回值: Proxy Expression,可以继续链式调用
示例:
// 包装 RegExpconstpattern=wrap(/^[a-z]+$/i);constinput=variable<string>();constisValid=pattern.test(input);constcompiled=compile(isValid,{ input });evaluate(compiled,{input: "hello"});// => trueevaluate(compiled,{input: "hello123"});// => false// 包装 Dateconstnow=wrap(newDate("2024-01-01"));constyear=now.getFullYear();// 包装数组conststaticNumbers=wrap([1,2,3,4,5]);constx=variable<number>();constdoubled=staticNumbers.map(lambda((n: number)=>expr({ n, x })("n * x")));// 包装对象constconfig=wrap({port: 8080,host: "localhost"});constport=config.port;// 直接访问属性// 包装 Mapconstmap=wrap(newMap([["a",1],["b",2],]));constkey=variable<string>();constvalue=map.get(key);// 链式调用consttext=wrap(" hello world ");constresult=text.trim().toUpperCase().replace("HELLO","HI");// => "HI WORLD"wrap() 函数可以将任意静态值转换为 Proxy Expression,使其可以像 Variable 一样调用方法和访问属性。这在需要对常量值执行操作时非常有用。
基本用法:
// 不使用 wrap(传统方式)interfaceValidator{match(text: string,pattern: RegExp): boolean;}constvalidator=variable<Validator>();constresult=validator.match("hello",/^[a-z]+$/i);// 使用 wrap(推荐方式)constpattern=wrap(/^[a-z]+$/i);constinput=variable<string>();constresult=pattern.test(input);支持的类型:
// 原始值constnum=wrap(42);conststr=wrap("hello");constbool=wrap(true);// Date 和 RegExpconstdate=wrap(newDate("2024-01-01"));constyear=date.getFullYear();constregex=wrap(/\d+/g);consttext=variable<string>();constmatches=text.match(regex);// BigIntconstbigNum=wrap(123456789n);constx=variable<bigint>();constsum=expr({ bigNum, x })("bigNum + x");// URLconsturl=wrap(newURL("https://example.com/path"));consthost=url.hostname;constport=url.port;// Map 和 Setconstmap=wrap(newMap([["key1",100],["key2",200],]));constkey=variable<string>();constvalue=map.get(key);constset=wrap(newSet([1,2,3]));constnum=variable<number>();consthas=set.has(num);// TypedArrayconstarr=wrap(newUint8Array([10,20,30]));constindex=variable<number>();constvalue=expr({ arr, index })("arr[index]");// 数组和对象constnumbers=wrap([1,2,3,4,5]);constmultiplier=variable<number>();constscaled=numbers.map(lambda((n: number)=>expr({ n, multiplier })("n * multiplier")));constconfig=wrap({port: 8080,host: "localhost"});constport=config.port;链式调用:
consttext=wrap(" Hello, World! ");constresult=text.trim().toLowerCase().replace("world","universe");// => "hello, universe!"与 variable 结合:
conststaticData=wrap({users: ["alice","bob","charlie"]});constindex=variable<number>();constusername=expr({ staticData, index })("staticData.users[index]");constcompiled=compile(username,{ index });evaluate(compiled,{index: 1});// => "bob"variable() 创建的变量是 Proxy 对象,支持链式属性访问和方法调用,所有操作都会自动转换为表达式。
属性访问:
constconfig=variable<{timeout: number;retries: number;database: {host: string;port: number;};}>();// 链式属性访问consttimeout=config.timeout;// 自动转换为表达式constdbHost=config.database.host;// 支持嵌套访问constcompiled=compile(timeout,{ config });constresult=evaluate(compiled,{config: {timeout: 5000,retries: 3,database: {host: "localhost",port: 5432}},});// => 5000方法调用:
constcalculator=variable<{add(a: number,b: number): number;multiply(x: number,y: number): number;}>();// 方法调用constsum=calculator.add(1,2);constproduct=calculator.multiply(5,3);// 链式方法调用constbuilder=variable<{setName(name: string): typeofbuilder;build(): {name: string};}>();constresult=builder.setName("test").build();// 编译并执行constcompiled=compile(sum,{ calculator });constvalue=evaluate(compiled,{calculator: {add: (a,b)=>a+b,multiply: (x,y)=>x*y,},});// => 3数组方法:
数组变量支持所有标准数组方法,并自动处理类型推导:
constnumbers=variable<number[]>();constusers=variable<{id: number;name: string}[]>();// mapconstdoubled=numbers.map((n)=>expr({ n })("n * 2"));// filterconstactiveUsers=users.filter((u)=>expr({ u })("u.active"));// reduceconstsum=numbers.reduce(lambda<[number,number],number>((acc,val)=>expr({ acc, val })("acc + val")),0);// find, some, every, sort 等constfirstMatch=users.find((u)=>expr({ u })("u.id === 1"));consthasAdmins=users.some((u)=>expr({ u })("u.role === 'admin'"));constallActive=users.every((u)=>expr({ u })("u.active"));constsorted=numbers.toSorted(lambda<[number,number],number>((a,b)=>expr({ a, b })("a - b")));表达式中可以直接使用以下内置对象(无需在上下文中定义):
Math,JSON,Date,RegExpNumber,String,Boolean,Array,Objectundefined,NaN,InfinityisNaN,isFinite,parseInt,parseFloat
constx=variable<number>();constsqrtExpr=expr({ x })("Math.sqrt(x)");constcompiled=compile(sqrtExpr,{ x });constresult=evaluate(compiled,{x: 16});// => 4算术运算符:
+,-,*,/,%,**(幂运算)
比较运算符:
==,===,!=,!==,<,>,<=,>=
逻辑运算符:
&&,||,!,??(空值合并)
位运算符:
&,|,^,~,<<,>>,>>>
其他运算符:
? :(三元表达式)in(属性存在检查)instanceof(类型检查)typeof(类型检测)?.(可选链)?.()(可选调用)?.[](可选元素访问)
语法特性:
- 对象字面量:
{ key: value, ... } - 数组字面量:
[element1, element2, ...] - 箭头函数:
(param) => expression - 函数调用:
func(arg1, arg2, ...) - 成员访问:
obj.prop,obj["prop"],arr[0] - 模板字面量(通过
t标签函数) - 分组括号:
(expression)
constscore=variable<number>();constgradeExpr=expr({ score })("score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : 'F'");constcompiled=compile(gradeExpr,{ score });constgrade=evaluate(compiled,{score: 85});// => "B"constnumbers=variable<number[]>();constsumExpr=expr({ numbers })("numbers.reduce((a, b) => a + b, 0)");constcompiled=compile(sumExpr,{ numbers });constsum=evaluate(compiled,{numbers: [1,2,3,4,5]});// => 15consta=variable<number>();constb=variable<number>();constsum=expr({ a, b })("a + b");constproduct=expr({ a, b })("a * b");constdifference=expr({ a, b })("a - b");constcomplex=expr({ sum, product, difference })("sum * product - difference");constcompiled=compile(complex,{ a, b });constresult=evaluate(compiled,{a: 2,b: 3});// => (2+3) * (2*3) - (2-3) = 5 * 6 - (-1) = 30 + 1 = 31编译器支持为 &&、||、?? 和三元表达式生成短路求值代码,避免不必要的计算:
consta=variable<boolean>();constb=variable<boolean>();// 逻辑或短路constorExpr=expr({ a, b })("a || b");constcompiled=compile(orExpr,{ a, b });// 当 a 为 true 时,b 不会被求值// 编译数据包含控制流节点:// [["a", "b"], ["br", "$[0]", 1], "$[1]", ["phi"]]// 空值合并constx=variable<number|null>();consty=variable<number>();constcoalesce=expr({ x, y })("x ?? y");// 三元表达式constcondition=variable<boolean>();constresult=variable<number>();constalternative=variable<number>();constternary=expr({ condition, result, alternative })("condition ? result : alternative");编译器自动将只被引用一次的子表达式内联到使用位置,减少中间计算:
constx=variable<number>();consty=variable<number>();constsum=expr({ x, y })("x + y");constproduct=expr({ x, y })("x * y");constresult=expr({ sum, product })("sum + product");// 自动内联后,编译结果为:// [["x", "y"], "($[0]+$[1])+($[0]*$[1])"]// 而不是 [["x", "y"], "$[0]+$[1]", "$[0]*$[1]", "$[2]+$[3]"]constcompiled=compile(result,{ x, y });constvalue=evaluate(compiled,{x: 2,y: 3});// => 11compile 函数支持直接编译包含 Proxy 的对象和数组:
constx=variable<number>();consty=variable<number>();constsum=expr({ x, y })("x + y");// 编译对象constobjCompiled=compile({result: sum,original: { x, y }},{ x, y });constobjResult=evaluate(objCompiled,{x: 10,y: 20});// => { result: 30, original: { x: 10, y: 20 }}// 编译数组constarrCompiled=compile([x,sum,100],{ x, y });constarrResult=evaluate(arrCompiled,{x: 5,y: 3});// => [5, 8, 100]编译后的数据可以轻松进行 JSON 序列化,适合网络传输或持久化存储:
// 编译表达式constcompiled=compile(result,{ x, y });// 序列化constjson=JSON.stringify(compiled);// "[["x","y"],"$[0]+$[1]","$[0]*$[1]","$[2]+$[3]"]"// 存储或传输...// 反序列化constdeserialized=JSON.parse(json);// 执行constvalue=evaluate(deserialized,{x: 5,y: 3});基础格式为 JSON 数组:[variableNames, ...expressions]
// 输入constsum=expr({ x, y })("x + y");constcompiled=compile(sum,{ x, y });// 输出// [["x", "y"], "$[0]+$[1]"]// $[0] 引用 x,$[1] 引用 y启用短路求值时,生成包含控制流节点的格式:
// 输入constresult=expr({ a, b })("a || b");constcompiled=compile(result,{ a, b });// 输出// [// ["a", "b"],// ["br", "$[0]", 1], // 如果 $[0] 为 truthy,跳过 1 条指令// "$[1]", // 否则求值 $[1]// ["phi"] // 取最近求值结果// ]控制流节点类型:
["br", condition, offset]- 条件跳转,条件为真时跳过 offset 条指令["jmp", offset]- 无条件跳转,跳过 offset 条指令["phi"]- 取最近求值结果(用于合并分支)
编译器会检测并报告以下错误:
constx=variable<number>();consty=variable<number>();// 错误:引用未定义的变量constinvalid=expr({ x, y })("x + y + z");compile(invalid,{ x, y });// => Error: Undefined variable(s): z// 错误:变量名冲突constxy=variable<number>();constconflict=expr({ xy, x })("xy + x");// 正确处理:编译器能区分 xy 和 xconstcompiled=compile(conflict,{ xy, x });// => [["xy", "x"], "$[0]+$[1]"]求值器会验证输入并报告运行时错误:
constx=variable<number>();consty=variable<number>();constsum=expr({ x, y })("x + y");constcompiled=compile(sum,{ x, y });// 错误:缺少必需变量evaluate(compiled,{x: 2});// => Error: Missing required variable: y// 错误:无效的编译数据evaluate([],{x: 1});// => Error: Invalid compiled data: must have at least variable names项目充分利用 TypeScript 的类型系统进行编译时检查和类型推导:
constx=variable<number>();consty=variable<string>();// 类型错误会在编译时捕获// const invalid = expr({ x, y })("z + y"); // Error: 'z' not in contextconstvalid=expr({ x })("-x");// 编译器推导为 numberconstformData=variable<{username: string;password: string;confirmPassword: string;age: number;}>();// 创建验证规则表达式constisUsernameValid=expr({ formData })("formData.username.length >= 3 && formData.username.length <= 20");constisPasswordValid=expr({ formData })("formData.password.length >= 8 && /[A-Z]/.test(formData.password)");constdoPasswordsMatch=expr({ formData })("formData.password === formData.confirmPassword");constisAgeValid=expr({ formData })("formData.age >= 18 && formData.age <= 120");constisFormValid=expr({
isUsernameValid,
isPasswordValid,
doPasswordsMatch,
isAgeValid,})("isUsernameValid && isPasswordValid && doPasswordsMatch && isAgeValid");// 编译一次,多次执行constcompiled=compile(isFormValid,{ formData });// 在表单输入时实时验证evaluate(compiled,{formData: {username: "john_doe",password: "Secure123",confirmPassword: "Secure123",age: 25,},});// => trueconstrawData=variable<any[]>();constconfig=variable<{minValue: number;maxValue: number;transform: (x: number)=>number;}>();// 构建数据处理管道constfiltered=rawData.filter(lambda<[any],boolean>((item)=>expr({ item, config })("item.value >= config.minValue && item.value <= config.maxValue")));consttransformed=filtered.map(lambda<[any],number>((item)=>expr({ item, config })("config.transform(item.value)")));constsorted=transformed.toSorted(lambda<[number,number],number>((a,b)=>expr({ a, b })("a - b")));constpipeline=compile(sorted,{ rawData, config });// 执行数据处理constresult=evaluate(pipeline,{rawData: [{value: 10},{value: 5},{value: 20},{value: 15}],config: {minValue: 8,maxValue: 18,transform: (x: number)=>x*2},});// => [10, 20, 30] (5 被过滤,10*2=20, 15*2=30, 20 被过滤)// 定义规则条件constuser=variable<{age: number;role: string;balance: number;}>();constisEligible=expr({ user })("(user.age >= 18 && user.age <= 65) && (user.role === 'premium' || user.balance > 10000)");constdiscountRate=expr({ user, isEligible })("isEligible ? (user.role === 'premium' ? 0.2 : 0.1) : 0");construle=compile(discountRate,{ user });// 应用规则constdiscount=evaluate(rule,{user: {age: 30,role: "premium",balance: 5000},});// => 0.2 (20% 折扣)- 编译时间:编译过程涉及依赖分析和拓扑排序,通常快速完成
- 执行时间:表达式通过
new Function()编译为原生 JavaScript,执行性能接近原生代码 - 内存占用:编译数据为纯 JSON,占用空间小,适合在网络上传输
- 缓存机制:求值器缓存已编译的函数,重复执行时性能更优
编译一次,多次执行:对于重复使用的表达式,先编译后多次求值
constcompiled=compile(expression,variables);// 缓存 compiled,多次调用 evaluateevaluate(compiled,values1);evaluate(compiled,values2);
利用短路求值:短路求值已默认启用,对于条件表达式可以避免不必要的计算
利用自动内联:编译器会自动内联只引用一次的子表达式,无需手动优化
优先使用 Proxy 链式调用:对于对象属性访问,使用
config.timeout比expr({ config })("config.timeout")更简洁且类型更安全
src/
├── index.ts # 导出入口
├── variable.ts # variable<T>() 函数
├── expr.ts # expr() 函数
├── template.ts # t() 标签模板函数
├── lambda.ts # lambda() 函数(数组方法支持)
├── compile.ts # 编译器(内联优化、短路求值)
├── evaluate.ts # 运行时求值
├── parser.ts # 表达式 AST 解析器
├── type-parser.ts # TypeScript 类型级表达式解析
├── proxy-variable.ts # Proxy 变量实现
├── proxy-metadata.ts # Proxy 元数据管理
└── types.ts # 类型定义(Variable、Expression、Lambda 等)
bun installbun testbun run lint
bun run type-checkbun run formatMIT
欢迎提交 Issue 和 Pull Request!