@marcbachmann/cel-js

A high-performance, zero-dependency implementation of the Common Expression Language (CEL) in JavaScript.
🚀 Use the CEL JS Playground to test expressions.
Overview
CEL (Common Expression Language) is a non-Turing complete language designed for simplicity, speed, safety, and portability. This JavaScript implementation provides a fast, lightweight CEL evaluator perfect for policy evaluation, configuration, and embedded expressions.
Features
- 🚀 Zero Dependencies - No external packages required
- ⚡ High Performance - Up to 22x faster evaluation, 3x faster parsing than alternatives
- 📦 ES Modules - Modern ESM with full tree-shaking support
- 🔒 Type Safe - Environment API with type checking for variables, custom types and functions
- 🎯 Most of the CEL Spec - Including macros, custom functions and types, optional chaining, input variables, and all operators
- 📘 TypeScript Support - Full type definitions included
Installation
npm install @marcbachmann/cel-js
Quick Start
import {evaluate} from '@marcbachmann/cel-js'
evaluate('1 + 2 * 3')
const allowed = evaluate(
'user.age >= 18 && "admin" in user.roles',
{user: {age: 30, roles: ['admin', 'user']}}
)
API
Simple Evaluation
import {evaluate, parse} from '@marcbachmann/cel-js'
evaluate('1 + 2')
evaluate('name + "!"', {name: 'Alice'})
const expr = parse('user.age >= minAge')
expr({user: {age: 25}, minAge: 18})
expr({user: {age: 16}, minAge: 18})
console.log(expr.ast)
const typeCheck = expr.check()
Environment API (Recommended)
For type-safe expressions with custom functions and operators:
import {Environment} from '@marcbachmann/cel-js'
const env = new Environment()
.registerVariable('user', 'map')
.registerConstant('minAge', 'int', 18n)
.registerFunction('isAdult(int): bool', age => age >= 18n)
.registerOperator('string * int', (str, n) => str.repeat(Number(n)))
env.evaluate('isAdult(user.age) && user.age >= minAge', {
user: {age: 25n}
})
env.evaluate('"Hi" * 3')
Register constants
Use registerConstant(name, type, value) to expose shared configuration without passing it through every evaluation context.
import {Environment} from '@marcbachmann/cel-js'
const env = new Environment()
.registerConstant('minAge', 'int', 18n)
env.evaluate('user.age >= minAge', {user: {age: 20n}})
Environment Options
new Environment({
unlistedVariablesAreDyn: false,
homogeneousAggregateLiterals: true,
enableOptionalTypes: true,
limits: {
maxAstNodes: 100000,
maxDepth: 250,
maxListElements: 1000,
maxMapEntries: 1000,
maxCallArguments: 32
}
})
- Set
homogeneousAggregateLiterals to false if you need aggregate literals to accept mixed element/key/value types without wrapping everything in dyn(...).
- Set
enableOptionalTypes to true to activate optional chaining.
Environment Methods
registerVariable(name, type) - Declare a variable with type checking
registerType(typename, constructor) - Register custom types
registerFunction(signature, handler) - Add custom functions
registerOperator(signature, handler) - Add custom operators
registerConstant(name, type, value) - Provide immutable values without passing them in context
clone() - Create a fast, isolated copy that stops the parent from registering more entries
hasVariable(name) - Check if variable is registered
parse(expression) - Parse expression for reuse
evaluate(expression, context) - Evaluate with context
check(expression) - Validate expression types without evaluation
registerFunction (sync & async)
registerFunction(signature, handler) accepts both synchronous and async handlers. When an async function (or a macro predicate/transform that uses async functions) participates in an expression, env.evaluate() returns a Promise that resolves with the final value. Consumers should await those evaluations when they register async behavior:
const env = new Environment()
.registerFunction('fetchUser(string): map', async (id) => {
const res = await fetch(`/users/${id}`)
return res.json()
})
const user = await env.evaluate('fetchUser(userId)', {userId: '42'})
Async handlers are primarily intended for latency-sensitive lookups (e.g., cache fetches, lightweight RPC). CEL’s goal is still deterministic, predictable evaluation, so avoid building expressions that trigger unbounded async work (like nested loops within macros or large fan-out requests) even though the engine will await those results.
Environment Cloning
import assert from 'node:assert/strict'
const parent = new Environment().registerVariable('user', 'map')
const child = parent.clone()
assert.throws(() => parent.registerVariable('foo', 'dyn'))
child
.registerFunction('isAdult(map): bool', (u) => u.age >= 18n)
.registerVariable('minAge', 'int')
child.evaluate('isAdult(user) && user.age >= minAge', {
user: {age: 20n},
minAge: 18n
})
Supported Types: int, uint, double, string, bool, bytes, list, map, timestamp, duration, null_type, type, dyn, or custom types
Type Checking
Validate expressions before evaluation to catch type errors early:
import {Environment, TypeError} from '@marcbachmann/cel-js'
const env = new Environment()
.registerVariable('age', 'int')
.registerVariable('name', 'string')
const result = env.check('age >= 18 && name.startsWith("A")')
if (result.valid) {
console.log(`Expression is valid, returns: ${result.type}`)
const value = env.evaluate('age >= 18 && name.startsWith("A")', {
age: 25n,
name: 'Alice'
})
} else {
console.error(`Type error: ${result.error.message}`)
}
const invalid = env.check('age + name')
console.log(invalid.valid)
console.log(invalid.error.message)
Benefits:
- Catch type mismatches before runtime
- Validate user-provided expressions safely
- Get inferred return types for expressions
- Better error messages with source location
Language Features
Operators
evaluate('10 + 5 - 3')
evaluate('10 * 5 / 2')
evaluate('10 % 3')
evaluate('5 > 3')
evaluate('5 >= 5')
evaluate('5 == 5')
evaluate('5 != 4')
evaluate('true && false')
evaluate('true || false')
evaluate('!false')
evaluate('5 > 3 ? "yes" : "no"')
evaluate('2 in [1, 2, 3]')
evaluate('"ell" in "hello"')
Data Types
evaluate('42')
evaluate('3.14')
evaluate('0xFF')
evaluate('"hello"')
evaluate('r"\\n"')
evaluate('"""multi\nline"""')
evaluate('b"hello"')
evaluate('b"\\xFF"')
evaluate('[1, 2, 3]')
evaluate('{name: "Alice"}')
evaluate('true')
evaluate('null')
Built-in Functions
evaluate('string(123)')
evaluate('int("42")')
evaluate('double("3.14")')
evaluate('bytes("hello")')
evaluate('dyn(42)')
evaluate('size([1, 2, 3])')
evaluate('size("hello")')
evaluate('size({a: 1, b: 2})')
evaluate('timestamp("2024-01-01T00:00:00Z")')
evaluate('type(42)')
evaluate('type("hello")')
String Methods
evaluate('"hello".contains("ell")')
evaluate('"hello".startsWith("he")')
evaluate('"hello".endsWith("lo")')
evaluate('"hello".matches("h.*o")')
evaluate('"hello".size()')
evaluate('"hello".indexOf("ll")')
evaluate('"hello world".indexOf("o", 5)')
evaluate('"hello".lastIndexOf("l")')
evaluate('"hello".substring(1)')
evaluate('"hello".substring(1, 4)')
List Methods
evaluate('[1, 2, 3].size()')
evaluate('["a", "b", "c"].join()')
evaluate('["a", "b", "c"].join(", ")')
Bytes Methods
evaluate('b"hello".size()')
evaluate('b"hello".string()')
evaluate('b"hello".hex()')
evaluate('b"hello".base64()')
evaluate('b"{\\"x\\": 42}".json()')
evaluate('b"hello".at(0)')
Timestamp Methods
All timestamp methods support an optional timezone parameter (e.g., "America/New_York", "UTC"):
const ctx = {t: new Date('2024-01-15T14:30:45.123Z')}
evaluate('t.getFullYear()', ctx)
evaluate('t.getMonth()', ctx)
evaluate('t.getDayOfMonth()', ctx)
evaluate('t.getDayOfWeek()', ctx)
evaluate('t.getDayOfYear()', ctx)
evaluate('t.getHours()', ctx)
evaluate('t.getMinutes()', ctx)
evaluate('t.getSeconds()', ctx)
evaluate('t.getMilliseconds()', ctx)
evaluate('t.getHours("America/New_York")', ctx)
Macros
const ctx = {
numbers: [1, 2, 3, 4, 5],
users: [
{name: 'Alice', admin: true},
{name: 'Bob', admin: false}
]
}
evaluate('has(user.email)', {user: {}})
evaluate('numbers.all(n, n > 0)', ctx)
evaluate('numbers.exists(n, n > 3)', ctx)
evaluate('numbers.exists_one(n, n == 3)', ctx)
evaluate('numbers.map(n, n * 2)', ctx)
evaluate('numbers.filter(n, n > 2)', ctx)
evaluate('users.filter(u, u.admin).map(u, u.name)', ctx)
evaluate('cel.bind(total, users.map(u, u.admin, u.score).sum(), total >= 90)', ctx)
evaluate('users.map(u, u.admin, u.name)', ctx)
Custom macros
You can register your own macros by declaring overloads that accept ast arguments. The macro handler executes at parse time and must return an object that provides both typeCheck and evaluate hooks; these hooks are invoked later during env.check() and env.evaluate() so the macro lines up with the regular type-checker/evaluator pipeline.
import {Environment} from '@marcbachmann/cel-js'
const env = new Environment()
env.registerFunction('macro(ast): dyn', ({ast, args}) => {
return {
firstArgument: args[0],
typeCheck(checker, macro, ctx) {
return checker.check(macro.firstArgument, ctx)
},
evaluate(evaluator, macro, ctx) {
return evaluator.eval(macro.firstArgument, ctx)
}
}
})
Custom Types
import {Environment} from '@marcbachmann/cel-js'
class Vector {
constructor(x, y) {
this.x = x
this.y = y
}
add(other) {
return new Vector(this.x + other.x, this.y + other.y)
}
}
const env = new Environment()
.registerType('Vector', Vector)
.registerVariable('v1', 'Vector')
.registerVariable('v2', 'Vector')
.registerOperator('Vector + Vector', (a, b) => a.add(b))
.registerFunction('magnitude(Vector): double', (v) =>
Math.sqrt(v.x * v.x + v.y * v.y)
)
const result = env.evaluate('magnitude(v1 + v2)', {
v1: new Vector(3, 4),
v2: new Vector(1, 2)
})
Performance
Benchmark results comparing against the cel-js package on Node.js v24.8.0 (Apple Silicon):
Parsing Performance
- Average: 3.1x faster (range: 0.76x - 14.8x)
- Simple expressions: 7-15x faster
- Array/Map creation: 8-10x faster
Evaluation Performance
- Average: 22x faster (range: 5.5x - 111x)
- Simple values: 64-111x faster
- Collections: 46-58x faster
- Complex logic: 5-14x faster
Highlights
| Simple number | 7.3x | 111x |
| Array creation | 10.1x | 57.9x |
| Map creation | 8.6x | 46x |
| Complex authorization | 1.3x | 5.5x |
Run benchmarks: npm run benchmark
Error Handling
import {evaluate, ParseError, EvaluationError, TypeError} from '@marcbachmann/cel-js'
try {
evaluate('invalid + + syntax')
} catch (error) {
if (error instanceof ParseError) {
console.error('Syntax error:', error.message)
} else if (error instanceof EvaluationError) {
console.error('Runtime error:', error.message)
}
}
const env = new Environment().registerVariable('x', 'int')
const result = env.check('x + "string"')
if (!result.valid && result.error instanceof TypeError) {
console.error('Type error:', result.error.message)
}
Examples
Authorization Rules
import {Environment} from '@marcbachmann/cel-js'
const authEnv = new Environment()
.registerVariable('user', 'map')
.registerVariable('resource', 'map')
const canEdit = authEnv.parse(`
user.isActive &&
(user.role == "admin" ||
user.id == resource.ownerId)
`)
canEdit({
user: {id: 123, role: 'user', isActive: true},
resource: {ownerId: 123}
})
Data Validation
import {Environment} from '@marcbachmann/cel-js'
const validator = new Environment()
.registerVariable('email', 'string')
.registerVariable('age', 'int')
.registerFunction('isValidEmail(string): bool',
email => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
)
const valid = validator.evaluate(
'isValidEmail(email) && age >= 18 && age < 120',
{email: 'user@example.com', age: 25n}
)
Feature Flags
import {parse} from '@marcbachmann/cel-js'
const flags = {
'new-dashboard': parse(
'user.betaUser || user.id in allowedUserIds'
),
'premium-features': parse(
'user.subscription == "pro" && !user.trialExpired'
)
}
function isEnabled(feature, context) {
return flags[feature]?.(context) ?? false
}
TypeScript
Full TypeScript support included:
import {Environment, evaluate, ParseError} from '@marcbachmann/cel-js'
const env = new Environment()
.registerVariable('count', 'int')
.registerFunction('multiplyByTwo(int): int', (x) => x * 2n)
const result: any = env.evaluate('multiplyByTwo(count)', {count: 21n})
Contributing
Contributions welcome! Please open an issue before submitting major changes.
npm test
npm run benchmark
npm run test:watch
License
MIT © Marc Bachmann
See Also