@hypatiatech/jexl
v4.10.5
Published
Javascript Expression Language: Powerful context-based expression parser and evaluator
Downloads
141
Maintainers
Readme
Jexl
A modern, TypeScript-first fork of the original Jexl library, brought to the XXI century.
Maintained by Pawel Uchida-Psztyc (@jarrodek) with modern tooling, enhanced type safety, and improved developer experience.
Original library created by Tom Shawver.
Javascript Expression Language: Powerful context-based expression parser and evaluator
Why This Fork?
This modernized version of Jexl brings several key improvements over the original:
🚀 Modern TypeScript Support
- Full TypeScript rewrite with comprehensive type definitions
- Enhanced type safety with
unknowntypes instead of unsafeany - Better IDE support with full IntelliSense and autocomplete
- Type-safe transform and function definitions - write your custom transforms with proper typing
🛠 Modern Development Experience
- ESM-first with proper ES module support
- Modern build tooling with up-to-date dependencies
- Comprehensive test coverage with modern testing framework
- Clean, maintainable codebase following current best practices
📦 Developer-Friendly
- Flexible function signatures - define transforms with specific parameter types
- Namespace support - organize functions and transforms with dot notation (e.g.,
Utils.String.upper,Api.User.getName()) - Better error handling and debugging experience
- Consistent API that works seamlessly in both Node.js and modern browsers
- Tree-shakeable for optimal bundle sizes
🔧 Enhanced Type Safety Example
// Before: Unsafe any types
jexl.addTransform('multiply', (val: any, factor: any) => val * factor)
// Now: Type-safe transforms
jexl.addTransform('multiply', (val: number, factor: number) => val * factor)
jexl.addTransform('upperCase', (val: string) => val.toUpperCase())
jexl.addTransform('formatDate', (val: Date, format: string) => /* ... */)Quick start
Use it with promises or synchronously:
const context = {
name: { first: 'Sterling', last: 'Archer' },
assoc: [
{ first: 'Lana', last: 'Kane' },
{ first: 'Cyril', last: 'Figgis' },
{ first: 'Pam', last: 'Poovey' }
],
age: 36
}
// Filter an array asynchronously...
await const res = jexl.eval('assoc[.first == "Lana"].last', context)
console.log(res) // Output: Kane
// Do math
await jexl.eval('age * (3 - 1)', context)
// 72
// Concatenate
await jexl.eval('name.first + " " + name["la" + "st"]', context)
// "Sterling Archer"
// Compound
await jexl.eval(
'assoc[.last == "Figgis"].first == "Cyril" && assoc[.last == "Poovey"].first == "Pam"',
context
)
// true
// Use array indexes
await jexl.eval('assoc[1]', context)
// { first: 'Cyril', last: 'Figgis' }
// Use conditional logic
await jexl.eval('age > 62 ? "retired" : "working"', context)
// "working"
// Transform
jexl.addTransform('upper', (val) => val.toUpperCase())
await jexl.eval('"duchess"|upper + " " + name.last|upper', context)
// "DUCHESS ARCHER"
// Namespace transforms (NEW!)
jexl.addTransform('String.upper', (val) => val.toUpperCase())
jexl.addTransform('String.repeat', (val, times) => val.repeat(times))
await jexl.eval('"hi"|String.repeat(3)|String.upper', context)
// "HIHIHI"
// Transform asynchronously, with arguments
jexl.addTransform('getStat', async (val, stat) => dbSelectByLastName(val, stat))
try {
const res = await jexl.eval('name.last|getStat("weight")', context)
console.log(res) // Output: 184
} catch (e) {
console.log('Database Error', e.stack)
}
// Functions too, sync or async, args or no args
jexl.addFunction('getOldestAgent', () => db.getOldestAgent())
await jexl.eval('age == getOldestAgent().age', context)
// false
// Namespace functions (NEW!)
jexl.addFunction('Math.max', (a, b) => Math.max(a, b))
jexl.addFunction('Utils.String.slugify', (text) =>
text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''))
await jexl.eval('Math.max(age, 25)', context)
// 36
// Add your own (a)synchronous operators
// Here's a case-insensitive string equality
jexl.addBinaryOp(
'_=',
20,
(left, right) => left.toLowerCase() === right.toLowerCase()
)
await jexl.eval('"Guest" _= "gUeSt"')
// true
// Compile your expression once, evaluate many times!
const { expr } = jexl
const danger = expr`"Danger " + place` // Also: jexl.compile('"Danger " + place')
await danger.eval({ place: 'zone' }) // Danger zone
await danger.eval({ place: 'ZONE!!!' }) // Danger ZONE!!! (Doesn't recompile the expression!)Template Literals (Backticks with {{ }})
You can embed expressions inside backtick-quoted template strings using {{ expr }} syntax. The text is returned with all interpolations evaluated and concatenated.
Examples:
await jexl.eval('`Total: {{5 + 5}} USD; Note: {{"Approved"}}`') // "Total: 10 USD; Note: Approved"
await jexl.eval('`Hello {{ user.name }}!`', { user: { name: 'Alice' } }) // "Hello Alice!"
await jexl.eval('"Invoice:" + ` Amount {{3}} USD`') // "Invoice: Amount 3 USD"Escapes inside templates:
- Use
\\to escape characters - Use ``` to render a backtick
- Use
\{{to render literal{{
Notes:
- Expressions inside
{{ }}are full Jexl expressions and evaluated in the same context. - Errors inside interpolations report positions relative to the original input.
- Undefined values resolve to empty strings: If a variable or expression evaluates to
undefinedinside a template, it will be rendered as an empty string rather than the literal "undefined":await jexl.eval('`Value: {{ missing }}`', {}) // "Value: " (not "Value: undefined")
Migration from Original Jexl
Upgrading from the original Jexl library is straightforward:
Package Installation
# Remove old package
npm uninstall jexl
# Install modern version
npm install @pawel-up/jexlImport Changes
// Before
const jexl = require('jexl')
// or
import jexl from 'jexl'
// Now
import { Jexl } from '@pawel-up/jexl'Enhanced Transform and Function Definitions
The API remains the same, but you can now use proper TypeScript types and namespace organization:
// Before: No type safety
jexl.addTransform('multiply', (val, factor) => val * factor)
// Now: Full type safety (optional but recommended)
jexl.addTransform('multiply', (val: number, factor: number): number => val * factor)
// NEW: Namespace support for better organization
jexl.addTransform('Math.multiply', (val: number, factor: number): number => val * factor)
jexl.addTransform('String.upper', (val: string): string => val.toUpperCase())
jexl.addFunction('Utils.String.slugify', (text: string): string =>
text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
)Backwards Compatibility
- ✅ 99% API compatible - existing code mostly works without changes. We removed the
evalSync()methods. - ✅ Same expression syntax - no need to update your expressions
- ✅ Same behavior - results are identical to the original library
- ✅ Same performance - optimized for modern JavaScript engines
Installation
Jexl works on the backend, and on the frontend if bundled using a bundler like Parcel or Webpack.
Install from npm:
npm install @pawel-up/jexl --saveand use it:
import { Jexl } from '@pawel-up/jexl'All the details
Unary Operators
| Operation | Symbol | | --------- | :----: | | Negate | ! |
Binary Operators
| Operation | Symbol |
| ---------------- | :----------: |
| Add, Concat | + |
| Subtract | - |
| Multiply | * |
| Divide | / |
| Divide and floor | // |
| Modulus | % |
| Power of | ^ |
| Logical AND | && |
| Logical OR | || |
| Logical AND (textual) | and |
| Logical OR (textual) | or |
| Logical XOR (textual) | xor |
Comparisons
| Comparison | Symbol | | -------------------------- | :----: | | Equal | == | | Not equal | != | | Greater than | > | | Greater than or equal | >= | | Less than | < | | Less than or equal | <= | | Element in array or string | in |
A note about in
The in operator can be used to check for a substring:
"Cad" in "Ron Cadillac", and it can be used to check for an array element:
"coarse" in ['fine', 'medium', 'coarse'].
Deep Comparison for Arrays and Objects:
The in operator uses deep comparison when searching arrays, meaning it can find objects and arrays within arrays by comparing their contents recursively:
// Deep comparison works for objects
{a: 1, b: 2} in [{a: 1, b: 2}, {a: 3}] // true
// Deep comparison works for arrays
[1, 2] in [[1, 2], [3, 4]] // true
// Deep comparison works for nested structures
{a: {x: 1}, b: [2, 3]} in [{a: {x: 1}, b: [2, 3]}, {a: {x: 2}}] // trueThe deep comparison:
- Recursively compares object properties and array elements
- Handles nested objects and arrays
- Compares Date objects by their timestamp
- Uses strict equality (
===) for primitive values
Performance Optimization:
For objects with an id property, the in operator uses an optimized comparison by comparing id values first, falling back to deep comparison if needed:
const item = { id: 2, name: 'John' }
const list = [{ id: 1, name: 'Jane' }, { id: 2, name: 'John' }]
item in list // true (compares by id for performance)Logical Operators with Validation Error Handling
Jexl supports textual logical operators (and, or, xor) that work seamlessly with validation errors. Validation errors are strings or objects where all values are strings (e.g., {en: "invalid", ar: "خاطئ"}).
and Operator
The and operator collects validation errors and returns them as an array:
| Expression | Result |
| ---------- | ------ |
| "invalid" and true | ["invalid"] |
| {en: "invalid", ar: "خاطئ"} and true | [{en: "invalid", ar: "خاطئ"}] |
| true and true | true |
| "invalid 1" and "invalid 2" | ["invalid 1", "invalid 2"] |
| "invalid 1" and "invalid 2" and "invalid 3" | ["invalid 1", "invalid 2", "invalid 3"] |
| "invalid" and false | false |
| false and "invalid" | false |
Behavior:
- If any operand is a validation error, all validation errors are collected into an array
- If all operands are truthy (non-validation errors), returns
true - If any operand is falsy (non-validation error), returns
false - Short-circuits when the left operand is falsy (non-validation error)
or Operator
The or operator returns the first truthy value, or collects validation errors:
| Expression | Result |
| ---------- | ------ |
| "invalid 1" or "invalid 2" | ["invalid 2"] |
| "invalid 1" or true | true |
| "invalid 1" or "invalid 2" or "invalid 3" | ["invalid 3"] |
| "invalid 1" or "invalid 2" or true | true |
| true or "invalid" | true |
Behavior:
- If the left operand is a validation error and the right is truthy, returns the right value
- If both operands are validation errors, returns an array with the right error
- If the left operand is truthy (non-validation error), returns the left value
- Otherwise returns the right value
xor Operator
The xor operator handles validation errors appropriately:
| Expression | Result |
| ---------- | ------ |
| "invalid" xor false | ["invalid"] |
| false xor "invalid" | ["invalid"] |
| "invalid 1" xor "invalid 2" | ["invalid 1", "invalid 2"] |
| "invalid" xor true | ["invalid"] |
| true xor false | true |
| true xor true | false |
Behavior:
- Collects all validation errors into an array
- Uses standard XOR logic (true if exactly one operand is truthy) when no validation errors are present
Validation Error Types
Validation errors can be:
- Strings: Any non-empty string (e.g.,
"invalid","error message") - Objects: Objects where all values are strings (e.g.,
{en: "invalid", ar: "خاطئ"})
Use Cases
These operators are particularly useful for form validation and error collection:
// Collect multiple validation errors
const errors = await jexl.eval('"Email is invalid" and "Password too short" and "Age must be 18+"')
console.log(errors) // ["Email is invalid", "Password too short", "Age must be 18+"]
// Multi-language validation errors
const errorObj = { en: "Invalid email", ar: "البريد الإلكتروني غير صحيح" }
const result = await jexl.eval('error and true', { error: errorObj })
console.log(result) // [{en: "Invalid email", ar: "البريد الإلكتروني غير صحيح"}]
// Chain validation checks
const validation = await jexl.eval(
'validateEmail(email) and validatePassword(password) and validateAge(age)',
{ email: '[email protected]', password: '123', age: 15 }
)
// Returns array of all validation errors, or true if all validations passTernary operator
Conditional expressions check to see if the first segment evaluates to a truthy value. If so, the consequent segment is evaluated. Otherwise, the alternate is. If the consequent section is missing, the test result itself will be used instead.
| Expression | Result | | --------------------------------- | ------ | | "" ? "Full" : "Empty" | Empty | | "foo" in "foobar" ? "Yes" : "No" | Yes | | {agent: "Archer"}.agent ?: "Kane" | Archer |
Native Types
| Type | Examples |
| -------- | :----------------------------: |
| Booleans | true, false |
| Strings | "Hello "user"", 'Hey there!' |
| Numerics | 6, -7.2, 5, -3.14159 |
| Objects | {hello: "world!"} |
| Arrays | ['hello', 'world!'] |
Groups
Parentheses work just how you'd expect them to:
| Expression | Result | | ----------------------------------- | :----- | | (83 + 1) / 2 | 42 | | 1 < 3 && (4 > 2 || 2 > 4) | true |
Identifiers
Access variables in the context object by just typing their name. Objects can be traversed with dot notation, or by using brackets to traverse to a dynamic property name.
Example context:
{
name: {
first: "Malory",
last: "Archer"
},
exes: [
"Nikolai Jakov",
"Len Trexler",
"Burt Reynolds"
],
lastEx: 2
}| Expression | Result | | ----------------- | ------------- | | name.first | Malory | | name['la' + 'st'] | Archer | | exes[2] | Burt Reynolds | | exes[lastEx - 1] | Len Trexler |
Collections
Collections, or arrays of objects, can be filtered by including a filter expression in brackets. Properties of each collection can be referenced by prefixing them with a leading dot. The result will be an array of the objects for which the filter expression resulted in a truthy value.
Example context:
{
employees: [
{first: 'Sterling', last: 'Archer', age: 36},
{first: 'Malory', last: 'Archer', age: 75},
{first: 'Lana', last: 'Kane', age: 33},
{first: 'Cyril', last: 'Figgis', age: 45},
{first: 'Cheryl', last: 'Tunt', age: 28}
],
retireAge: 62
}| Expression | Result | | --------------------------------------------- | ------------------------------------------------------------------------------------- | | employees[.first == 'Sterling'] | [{first: 'Sterling', last: 'Archer', age: 36}] | | employees[.last == 'Tu' + 'nt'].first | Cheryl | | employees[.age >= 30 && .age < 40] | [{first: 'Sterling', last: 'Archer', age: 36},{first: 'Lana', last: 'Kane', age: 33}] | | employees[.age >= 30 && .age < 40][.age < 35] | [{first: 'Lana', last: 'Kane', age: 33}] | | employees[.age >= retireAge].first | Malory |
Array Projection (Mapper)
When a filter expression returns non-boolean values, the collection acts as a mapper, returning an array of the evaluated expression results instead of filtering:
| Expression | Result | | --------------------------------------------- | ------------------------------------------------------------------------------------- | | employees[.first] | ['Sterling', 'Malory', 'Lana', 'Cyril', 'Cheryl'] | | employees[.first + " " + .last] | ['Sterling Archer', 'Malory Archer', 'Lana Kane', 'Cyril Figgis', 'Cheryl Tunt'] | | employees[.age * 2] | [72, 150, 66, 90, 56] | | employees[{value: .id, label: .name}] | [{value: 1, label: 'Sterling'}, {value: 2, label: 'Malory'}, ...] |
Behavior:
- If all filter results are booleans: filters the collection (returns matching objects)
- If any filter result is non-boolean: projects the collection (returns array of evaluated values)
Object Literal Mapping:
You can map array elements to objects using object literals with relative identifiers. Each array element becomes the relative context for evaluating the object literal properties:
const context = {
members: {
value: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
}
}
await jexl.eval('members.value[{value: .id, label: .name}]', context)
// Returns: [{value: 1, label: 'Alice'}, {value: 2, label: 'Bob'}]Object literal mapping supports:
- Computed values:
items[{id: .id, fullName: .name + " - $" + .price}] - Nested property access:
users[{first: .profile.firstName, last: .profile.lastName}] - Any expression: Object literal property values can contain any valid Jexl expression using relative identifiers
Spread Operator
Jexl supports the spread operator (...) for arrays and objects, allowing you to combine and merge collections:
Array Spread:
const context = { a: [1, 2], b: [3, 4], s: 'ab' }
// Spread arrays
await jexl.eval('[...a, 5, ...b]', context) // [1, 2, 5, 3, 4]
// Spread strings (iterable)
await jexl.eval('[...s, "c"]', context) // ['a', 'b', 'c']Object Spread:
const context = {
a: { x: 1, y: 2 },
b: { y: 9, z: 3 }
}
// Spread objects (later properties override earlier ones)
await jexl.eval('{...a, y: 5, ...b, x: 7}', context)
// { x: 7, y: 9, z: 3 }
// Order: a spreads first (x:1, y:2), then y:5, then b spreads (y:9, z:3), then x:7Spread in Object Literals:
The spread operator can be used within object literals to merge properties:
const context = { defaults: { theme: 'dark', lang: 'en' }, overrides: { lang: 'fr' } }
await jexl.eval('{...defaults, ...overrides}', context)
// { theme: 'dark', lang: 'fr' }Notes:
- Spread operator works with arrays, strings (as iterables), and objects
- For objects, later properties override earlier ones when there are conflicts
- Throws an error if attempting to spread a non-iterable value in arrays
Transforms
The power of Jexl is in transforming data, synchronously or asynchronously.
Transform functions take one or more arguments: The value to be transformed,
followed by anything else passed to it in the expression. They must return
either the transformed value, or a Promise that resolves with the transformed
value. Add them with jexl.addTransform(name, function).
Transform functions can also access the current execution context through this.context. See the Accessing Context in Functions and Transforms section under Functions for more details.
jexl.addTransform('split', (val, char) => val.split(char))
jexl.addTransform('lower', (val) => val.toLowerCase())
// Transform accessing context
jexl.addTransform('formatCurrency', function(val) {
const currency = this.context.currency || 'USD'
return `${currency} ${val}`
})| Expression | Result |
| ---------------------------------------- | --------------------- |
| "Pam Poovey" \| lower \| split[' '](1) | poovey |
| "password==guest" \| split('=' + '=') | ['password', 'guest'] |
Advanced Transforms
Using Transforms, Jexl can support additional string formats like embedded JSON, YAML, XML, and more. The following, with the help of the xml2json module, allows XML to be traversed just as easily as plain javascript objects:
const xml2json = require('xml2json')
jexl.addTransform('xml', (val) => xml2json.toJson(val, { object: true }))
const context = {
xmlDoc: `
<Employees>
<Employee>
<FirstName>Cheryl</FirstName>
<LastName>Tunt</LastName>
</Employee>
<Employee>
<FirstName>Cyril</FirstName>
<LastName>Figgis</LastName>
</Employee>
</Employees>`,
}
var expr = 'xmlDoc|xml.Employees.Employee[.LastName == "Figgis"].FirstName'
jexl.eval(expr, context).then(console.log) // Output: CyrilNamespace Functions and Transforms
Jexl supports organizing functions and transforms into namespaces using dot notation. This allows you to create hierarchical structures and avoid naming conflicts in large applications.
Namespace Functions
Functions can be organized into namespaces by using dot-separated names when registering them:
// Register namespace functions
jexl.addFunction('Math.max', (a, b) => Math.max(a, b))
jexl.addFunction('Utils.String.slugify', (text) =>
text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
)
jexl.addFunction('Api.User.getName', async (userId) => {
const user = await fetchUser(userId)
return user.name
})
// Use namespace functions in expressions
await jexl.eval('Math.max(10, 25)') // 35
await jexl.eval('Utils.String.slugify("Hello World!")') // "hello-world"
await jexl.eval('Api.User.getName(123)') // "John Doe"Namespace Transforms
Transforms can also be organized into namespaces and used with the pipe operator:
// Register namespace transforms
jexl.addTransform('String.upper', (val) => val.toUpperCase())
jexl.addTransform('String.lower', (val) => val.toLowerCase())
jexl.addTransform('String.repeat', (val, times) => val.repeat(times))
jexl.addTransform('Utils.Text.capitalize', (text) => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase())
jexl.addTransform('Format.Date.iso', (date) => new Date(date).toISOString())
// Use namespace transforms in expressions
await jexl.eval('"hello world"|String.upper') // "HELLO WORLD"
await jexl.eval('"HELLO WORLD"|String.lower') // "hello world"
await jexl.eval('"hi"|String.repeat(3)') // "hihihi"
await jexl.eval('"hello world"|Utils.Text.capitalize') // "Hello world"
await jexl.eval('"2024-01-01"|Format.Date.iso') // "2024-01-01T00:00:00.000Z"Deeply Nested Namespaces
Namespaces can be nested to any depth:
// Deep namespace registration
jexl.addFunction('Company.Departments.HR.getEmployeeCount', () => 150)
jexl.addTransform('Data.Validation.Email.isValid', (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
// Usage in expressions
await jexl.eval('Company.Departments.HR.getEmployeeCount()') // 150
await jexl.eval('"[email protected]"|Data.Validation.Email.isValid') // trueChaining Namespace Transforms
Namespace transforms can be chained just like regular transforms:
// Register multiple namespace transforms
jexl.addTransform('String.trim', (val) => val.trim())
jexl.addTransform('String.upper', (val) => val.toUpperCase())
jexl.addTransform('String.split', (val, separator) => val.split(separator))
// Chain namespace transforms
await jexl.eval('" hello world "|String.trim|String.upper|String.split(" ")')
// Result: ["HELLO", "WORLD"]Namespace Transforms with Arguments
Namespace transforms support arguments just like regular transforms:
// Transforms with multiple arguments
jexl.addTransform('String.padStart', (val, length, padString) => val.padStart(length, padString))
jexl.addTransform('Array.slice', (arr, start, end) => arr.slice(start, end))
// Usage with arguments
await jexl.eval('"5"|String.padStart(3, "0")') // "005"
await jexl.eval('[1,2,3,4,5]|Array.slice(1, 4)') // [2,3,4]Mixed Namespace and Regular Functions/Transforms
You can mix namespace and regular functions/transforms in the same expression:
// Register both types
jexl.addFunction('min', Math.min)
jexl.addFunction('Utils.Math.average', (arr) => arr.reduce((a, b) => a + b) / arr.length)
jexl.addTransform('sort', (arr) => [...arr].sort())
jexl.addTransform('Array.reverse', (arr) => [...arr].reverse())
// Mix in expressions
await jexl.eval('min(Utils.Math.average([1,2,3]), 5)') // 2
await jexl.eval('[3,1,4,2]|sort|Array.reverse') // [4,3,2,1]Namespace Organization Best Practices
Consider organizing your functions and transforms by domain or functionality:
// By domain
jexl.addFunction('User.authenticate', authenticateUser)
jexl.addFunction('User.authorize', authorizeUser)
jexl.addFunction('Product.getPrice', getProductPrice)
jexl.addFunction('Order.calculate', calculateOrder)
// By data type
jexl.addTransform('String.capitalize', capitalizeString)
jexl.addTransform('String.truncate', truncateString)
jexl.addTransform('Array.unique', uniqueArray)
jexl.addTransform('Array.flatten', flattenArray)
jexl.addTransform('Date.format', formatDate)
jexl.addTransform('Date.addDays', addDaysToDate)
// By utility category
jexl.addFunction('Validation.isEmail', isValidEmail)
jexl.addFunction('Validation.isPhoneNumber', isValidPhoneNumber)
jexl.addTransform('Format.currency', formatCurrency)
jexl.addTransform('Format.percentage', formatPercentage)Migration from Non-Namespaced Functions/Transforms
Existing non-namespaced functions and transforms continue to work unchanged:
// Existing code continues to work
jexl.addFunction('myFunc', () => 'old way')
jexl.addTransform('myTransform', (val) => val + ' transformed')
await jexl.eval('myFunc()') // "old way"
await jexl.eval('"test"|myTransform') // "test transformed"
// New namespace versions can coexist
jexl.addFunction('Utils.myFunc', () => 'new way')
jexl.addTransform('Utils.myTransform', (val) => val + ' namespace transformed')
await jexl.eval('Utils.myFunc()') // "new way"
await jexl.eval('"test"|Utils.myTransform') // "test namespace transformed"Functions
While Transforms are the preferred way to change one value into another value,
Jexl also allows top-level expression functions to be defined. Use these to
provide access to functions that either don't require an input, or require
multiple equally-important inputs. They can be added with
jexl.addFunction(name, function). Like transforms, functions can return a
value, or a Promise that resolves to the resulting value.
jexl.addFunction('min', Math.min)
jexl.addFunction('expensiveQuery', async () => db.runExpensiveQuery())| Expression | Result | | --------------------------------------------- | ------------------------- | | min(4, 2, 19) | 2 | | counts.missions || expensiveQuery() | Query only runs if needed |
Accessing Context in Functions and Transforms
Functions and transforms added via addFunction() or addTransform() can access the current execution context through this.context. This allows your custom functions to read values from the context object without requiring them as explicit parameters.
Important: Use regular function declarations, not arrow functions, to access this.context. Arrow functions don't have their own this binding and won't be able to access the context.
// Function accessing context
jexl.addFunction('getUserName', function() {
return this.context.user.name
})
const context = { user: { name: 'John Doe' } }
await jexl.eval('getUserName()', context) // "John Doe"
// Transform accessing context
jexl.addTransform('formatCurrency', function(val) {
const currency = this.context.currency || 'USD'
const symbol = currency === 'EUR' ? '€' : '$'
return `${symbol}${val.toFixed(2)}`
})
await jexl.eval('100|formatCurrency', { currency: 'USD' }) // "$100.00"
await jexl.eval('100|formatCurrency', { currency: 'EUR' }) // "€100.00"
// Function with arguments and context access
jexl.addFunction('multiplyByFactor', function(value) {
const factor = this.context.factor || 1
return value * factor
})
await jexl.eval('multiplyByFactor(10)', { factor: 5 }) // 50
// Complex example: accessing nested context properties
jexl.addFunction('getSetting', function(key) {
return this.context.settings?.[key] || null
})
const context = {
settings: {
theme: 'dark',
language: 'en'
}
}
await jexl.eval('getSetting("theme")', context) // "dark"
await jexl.eval('getSetting("language")', context) // "en"
// Works with async functions too
jexl.addFunction('getAsyncValue', async function() {
await someAsyncOperation()
return this.context.asyncValue
})
// Works with namespace functions and transforms
jexl.addFunction('Utils.getUser', function() {
return this.context.user
})
jexl.addTransform('Format.currency', function(val) {
const currency = this.context.currency || 'USD'
return `${currency} ${val}`
})Note: The context is isolated per evaluation. Each call to eval() receives its own context binding, so functions will always access the context from the current evaluation.
Context
Variable contexts are straightforward Javascript objects that can be accessed in the expression, but they have a hidden feature: they can include a Promise object, and when that property is used, Jexl will wait for the Promise to resolve and use that value!
API Reference
Jexl Instance
jexl.Jexl
A reference to the Jexl constructor. To maintain separate instances of Jexl
with each maintaining its own set of transforms, simply re-instantiate with
new jexl.Jexl().
jexl.addBinaryOp({string} operator, {number} precedence, {function} fn, {boolean} [manualEval])
Adds a binary operator to the Jexl instance. A binary operator is one that
considers the values on both its left and right, such as "+" or "==", in order
to calculate a result. The precedence determines the operator's position in the
order of operations (please refer to lib/grammar.js to see the precedence of
existing operators). The provided function will be called with two arguments:
a left value and a right value. It should return either the resulting value,
or a Promise that resolves to the resulting value.
If manualEval is true, the left and right arguments will be wrapped in
objects with an eval function. Calling left.eval() or right.eval() will
return a promise that resolves to that operand's actual value. This is useful to
conditionally evaluate operands, and is how && and || work.
jexl.addUnaryOp({string} operator, {function} fn)
Adds a unary operator to the Jexl instance. A unary operator is one that considers only the value on its right, such as "!", in order to calculate a result. The provided function will be called with one argument: the value to the operator's right. It should return either the resulting value, or a Promise that resolves to the resulting value.
jexl.addFunction({string} name, _{function} func)
Adds an expression function to this Jexl instance. The name can be a simple identifier (e.g., 'myFunction') or a namespace path using dot notation (e.g., 'Utils.String.slugify'). See the Functions and Namespace Functions and Transforms sections above for information on the structure of an expression function.
jexl.addFunctions({{}} map)
Adds multiple functions from a supplied map of function name to expression function.
jexl.addTransform({string} name, {function} transform)
Adds a transform function to this Jexl instance. The name can be a simple identifier (e.g., 'myTransform') or a namespace path using dot notation (e.g., 'String.upper'). See the Transforms and Namespace Functions and Transforms sections above for information on the structure of a transform function.
jexl.addTransforms({{}} map)
Adds multiple transforms from a supplied map of transform name to transform function.
jexl.compile({string} expression)
Constructs an Expression object around the given Jexl expression string.
Expression objects allow a Jexl expression to be compiled only once but
evaluated many times. See the Expression API below. Note that the only
difference between this function and jexl.createExpression is that this
function will immediately compile the expression, and throw any errors
associated with invalid expression syntax.
jexl.createExpression({string} expression)
Constructs an Expression object around the given Jexl expression string. Expression objects allow a Jexl expression to be compiled only once but evaluated many times. See the Expression API below.
jexl.getTransform({string} name)
Returns {function|undefined}. Gets a previously set transform function,
or undefined if no function of that name exists.
jexl.eval({string} expression, {{}} [context])
Returns {Promise<*>}. Evaluates an expression. The context map is optional.
jexl.expr: tagged template literal
A convenient bit of syntactic sugar for jexl.createExpression
const someNumber = 10
const expression = jexl.expr`5 + ${someNumber}`
console.log(await expression.eval()) // 15Note that expr will stay bound to its associated Jexl instance even if it's
pulled out of context:
const { expr } = jexl
jexl.addTransform('double', (val) => val * 2)
const expression = expr`2|double`
console.log(await expression.eval()) // 4jexl.removeOp({string} operator)
Removes a binary or unary operator from the Jexl instance. For example, "^" can be passed to eliminate the "power of" operator.
Expression
Expression objects are created via jexl.createExpression, jexl.compile, or
jexl.expr, and are a convenient way to ensure jexl expressions compile only
once, even if they're evaluated multiple times.
expression.compile()
Returns self {Expression}. Forces the expression to compile, even if it
was compiled before. Note that each compile will happen with the latest grammar
and transforms from the associated Jexl instance.
expression.eval({{}} [context])
Returns {Promise<*>}. Evaluates the expression. The context map is
optional.
Expression Validation
This modernized version of Jexl includes a powerful Validator class that helps you validate expressions before evaluation, providing comprehensive error reporting and analysis.
Validator Class
The Validator class provides static methods to validate Jexl expressions, offering detailed feedback about syntax errors, semantic issues, and potential problems.
Key Features
- 🔍 Comprehensive Validation: Checks syntax, semantics, and context usage
- 🚨 Detailed Error Reporting: Provides specific error messages with position information
- ⚠️ Warning System: Identifies potential issues and performance concerns
- ℹ️ Informational Messages: Offers suggestions for improvements
- 🧹 Automatic Whitespace Trimming: Cleans expressions before validation
- 🔧 Context-Agnostic Mode: Validates expressions without requiring specific context
- JavaScript-aligned Logic: Correctly distinguishes between
nullandundefinedand handles&&/||operator precedence just like in JS. - Powerful Operators: Full support for unary minus and plus on expressions (e.g.,
-myFunc(),+variable). - Robust Parsing: Handles complex cases like negative numbers in function arguments (
myFunc(5, -3)), ternary operators after function calls, and leading decimal numbers (.5). - Clear Error Handling: Provides precise errors for ambiguous syntax like
2++2.
Basic Usage
import { Validator, Jexl } from '@pawel-up/jexl'
const jexl = new Jexl()
const validator = new Validator(jexl.grammar)
// Validate a simple expression
const result = validator.validate('name.first + " " + name.last')
console.log(result.isValid) // true
console.log(result.errors) // []
// Validate with context
const context = { name: { first: 'John', last: 'Doe' } }
const result2 = validator.validate('name.middle', context)
console.log(result2.isValid) // false
console.log(result2.errors[0].message) // "Property 'middle' not found in context object 'name'"Validation Result
The validate method returns a ValidationResult object with the following properties:
interface ValidationResult {
isValid: boolean // Overall validation status
errors: ValidationIssue[] // Critical errors that prevent execution
warnings: ValidationIssue[] // Non-critical issues that might cause problems
info: ValidationIssue[] // Informational suggestions
trimmedExpression: string // Expression after automatic whitespace trimming
}
interface ValidationIssue {
severity: 'error' | 'warning' | 'info'
message: string
position?: number // Character position in expression
line?: number // Line number (for multi-line expressions)
column?: number // Column number
}Automatic Whitespace Trimming
The Validator automatically trims leading and trailing whitespace from expressions before validation:
// These expressions are equivalent after trimming
validator.validate('name.first')
validator.validate(' name.first ')
validator.validate('\t\nname.first\n\t')Context-Agnostic Validation
Use allowUndefinedContext: true to validate expressions without providing specific context:
// Validate syntax only, ignore missing context
const result = validator.validate('user.profile.email', undefined, {
allowUndefinedContext: true,
})
console.log(result.isValid) // true (if syntax is correct)
console.log(result.warnings) // May contain warnings about undefined contextValidation Examples
// Syntax error
const syntaxError = validator.validate('name.first +')
console.log(syntaxError.errors[0].message) // "Unexpected end of expression"
// Context validation
const context = { user: { name: 'John' } }
const contextError = validator.validate('user.age', context)
console.log(contextError.errors[0].message) // "Property 'age' not found in context object 'user'"
// Performance warning
const perfWarning = validator.validate('users[.name == "John" && .active == true]')
console.log(perfWarning.warnings[0].message) // "Complex filter expression may impact performance"
// Style suggestion
const styleInfo = validator.validate('name["first"]')
console.log(styleInfo.info[0].message) // "Consider using dot notation: name.first"Advanced Usage
// Validate multiple expressions
const expressions = ['user.name', 'user.age > 18', 'users[.active == true].length']
expressions.forEach((expr, index) => {
const result = validator.validate(expr, context)
if (!result.isValid) {
console.log(`Expression ${index + 1} has errors:`, result.errors)
}
if (result.warnings.length > 0) {
console.log(`Expression ${index + 1} has warnings:`, result.warnings)
}
})
// Custom validation with options
const result = validator.validate(' user.email ', undefined, {
allowUndefinedContext: true,
})
console.log(`Original: "${result.trimmedExpression}"`)
console.log(`Trimmed: "${result.trimmedExpression}"`)
console.log(`Valid: ${result.isValid}`)Validator API Reference
validate(expression, context?, options?)
Parameters:
expression{string}: The Jexl expression to validatecontext{object}: Optional context object for validationoptions{object}: Optional validation optionsallowUndefinedContext{boolean}: Allow undefined context properties (default: false)
Returns: ValidationResult object with validation details
ValidationResult Properties
isValid{boolean}: True if expression has no errorserrors{ValidationIssue[]}: Array of critical validation errorswarnings{ValidationIssue[]}: Array of non-critical warningsinfo{ValidationIssue[]}: Array of informational suggestionstrimmedExpression{string}: Expression after whitespace trimming
Expression Autocomplete
This modernized version of Jexl includes a powerful Autocomplete engine that provides intelligent suggestions while users write Jexl expressions. The autocomplete system analyzes the expression at a given cursor position and returns context-aware suggestions with rich metadata.
Key Features
- 🔍 Context-aware suggestions: Suggests available variables, functions, transforms based on current context and cursor position
- 🎯 Syntax-aware suggestions: Suggests operators, keywords, closing brackets/parens
- 🏷️ Type-aware suggestions: Suggests properties based on inferred types from context objects
- 📝 Rich metadata: Each suggestion includes type, description, signature, and documentation
- 🎨 Namespace support: Full support for namespace functions and transforms
- 🚀 Performance optimized: Fast suggestions even with large context objects
Basic Usage
import { Jexl, Autocomplete } from '@pawel-up/jexl'
const jexl = new Jexl()
// Get suggestions for dot notation
const result = jexl.autocomplete('user.', 5, {
user: { name: 'John', age: 30 },
})
console.log(result.suggestions)
// [{ label: 'name', value: 'name', type: 'property', description: 'Property of type string' }, ...]
// Get suggestions for transforms
jexl.addTransform('upper', (val: string) => val.toUpperCase())
const transformResult = jexl.autocomplete('value|', 6)
console.log(transformResult.suggestions)
// [{ label: 'upper', value: 'upper', type: 'transform', description: 'Transform function' }, ...]Autocomplete Result Structure
The autocomplete method returns an AutocompleteResult object:
interface AutocompleteResult {
suggestions: AutocompleteSuggestion[] // Array of suggestions
triggerCharacter?: string // What triggered autocomplete (., |, etc.)
replaceRange?: { start: number; end: number } // Range to replace when suggestion is selected
}
interface AutocompleteSuggestion {
label: string // Display text shown to user
value: string // Text to insert when suggestion is selected
type: SuggestionType // 'property', 'function', 'transform', 'operator', etc.
description?: string // Human-readable description
signature?: string // Function/transform signature
documentation?: string // Detailed documentation
detail?: string // Additional context information
sortText?: string // Custom sort order (lower = higher priority)
filterText?: string // Custom filter text for matching
}Context-Aware Suggestions
Dot Notation Property Access
const context = {
user: {
profile: {
personal: { firstName: 'John', lastName: 'Doe' },
contact: { email: '[email protected]', phone: '123-456-7890' },
},
},
}
// Get property suggestions
const result = jexl.autocomplete('user.profile.', 13, context)
console.log(result.suggestions)
// [
// { label: 'personal', value: 'personal', type: 'property', description: 'Property of type object' },
// { label: 'contact', value: 'contact', type: 'property', description: 'Property of type object' }
// ]
// Deep nested property access
const deepResult = jexl.autocomplete('user.profile.personal.', 22, context)
console.log(deepResult.suggestions)
// [
// { label: 'firstName', value: 'firstName', type: 'property', description: 'Property of type string' },
// { label: 'lastName', value: 'lastName', type: 'property', description: 'Property of type string' }
// ]Transform Suggestions
// Register transforms
jexl.addTransform('upper', (val: string) => val.toUpperCase())
jexl.addTransform('lower', (val: string) => val.toLowerCase())
jexl.addTransform('trim', (val: string) => val.trim())
jexl.addTransform('String.upper', (val: string) => val.toUpperCase())
jexl.addTransform('String.lower', (val: string) => val.toLowerCase())
// Get transform suggestions after pipe operator
const result = jexl.autocomplete('value|', 6)
console.log(result.suggestions)
// [
// { label: 'upper', value: 'upper', type: 'transform', description: 'Transform function' },
// { label: 'lower', value: 'lower', type: 'transform', description: 'Transform function' },
// { label: 'trim', value: 'trim', type: 'transform', description: 'Transform function' },
// { label: 'String.upper', value: 'String.upper', type: 'transform', description: 'Transform function' },
// { label: 'String.lower', value: 'String.lower', type: 'transform', description: 'Transform function' }
// ]Namespace Transform Suggestions
// Get suggestions for namespace transforms
const namespaceResult = jexl.autocomplete('String.', 7)
console.log(namespaceResult.suggestions)
// [
// { label: 'upper', value: 'upper', type: 'transform', description: 'Transform function in String namespace' },
// { label: 'lower', value: 'lower', type: 'transform', description: 'Transform function in String namespace' }
// ]Function Suggestions
// Register functions
jexl.addFunction('max', (...args: number[]) => Math.max(...args))
jexl.addFunction('min', (...args: number[]) => Math.min(...args))
jexl.addFunction('Math.max', (a: number, b: number) => Math.max(a, b))
jexl.addFunction('Utils.String.slugify', (text: string) =>
text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
)
// Get function suggestions
const result = jexl.autocomplete('', 0)
console.log(result.suggestions)
// [
// { label: 'max', value: 'max', type: 'function', description: 'Function' },
// { label: 'min', value: 'min', type: 'function', description: 'Function' },
// { label: 'Math.max', value: 'Math.max', type: 'function', description: 'Function' },
// { label: 'Utils.String.slugify', value: 'Utils.String.slugify', type: 'function', description: 'Function' }
// ]
// Get namespace function suggestions
const namespaceResult = jexl.autocomplete('Math.', 5)
console.log(namespaceResult.suggestions)
// [
// { label: 'max', value: 'max', type: 'function', description: 'Function in Math namespace' }
// ]Advanced Features
Partial Input Filtering
The autocomplete engine automatically filters suggestions based on partial input:
const context = { name: 'John', age: 30, active: true }
// Filter suggestions that start with 'na'
const result = jexl.autocomplete('na', 2, context)
console.log(result.suggestions)
// [{ label: 'name', value: 'name', type: 'identifier', description: 'Variable of type string' }]
// Note: 'age' and 'active' are filtered outReplace Range Detection
The autocomplete engine provides the exact range of text to replace when a suggestion is selected:
const result = jexl.autocomplete('na', 2, context)
console.log(result.replaceRange)
// { start: 0, end: 2 } - Replace characters 0-2 with the selected suggestionTrigger Character Detection
The engine identifies what character triggered the autocomplete:
// Dot notation
const dotResult = jexl.autocomplete('user.', 5, context)
console.log(dotResult.triggerCharacter) // '.'
// Pipe operator
const pipeResult = jexl.autocomplete('value|', 6)
console.log(pipeResult.triggerCharacter) // '|'Standalone Autocomplete Class
You can also use the Autocomplete class independently:
import { Autocomplete } from '@pawel-up/jexl'
const jexl = new Jexl()
jexl.addFunction('test', () => 'test')
jexl.addTransform('upper', (val: string) => val.toUpperCase())
const autocomplete = new Autocomplete(jexl.grammar)
const result = autocomplete.getSuggestions('', 0)
console.log(result.suggestions)
// Get suggestions without needing a Jexl instanceError Handling
The autocomplete engine gracefully handles errors and edge cases:
// Invalid cursor positions
const result1 = jexl.autocomplete('name', 10, context) // Cursor beyond expression length
console.log(result1.suggestions) // []
// Malformed expressions
const result2 = jexl.autocomplete('user..', 6, context) // Double dots
console.log(result2.suggestions) // [] (gracefully handled)
// Inside string literals
const result3 = jexl.autocomplete('"hello ', 7, context)
console.log(result3.suggestions) // [] (no suggestions inside strings)Integration with IDEs and Editors
The autocomplete engine is designed to integrate seamlessly with code editors and IDEs:
// Example integration with a text editor
function setupAutocomplete(editor: TextEditor, context: any) {
editor.onCursorChange((position) => {
const expression = editor.getText()
const result = jexl.autocomplete(expression, position, context)
if (result.suggestions.length > 0) {
showSuggestions(result.suggestions, result.replaceRange)
}
})
}
function showSuggestions(suggestions: AutocompleteSuggestion[], replaceRange?: { start: number; end: number }) {
// Display suggestions in editor's autocomplete popup
// Use replaceRange to determine what text to replace
// Use suggestion metadata for rich display (icons, descriptions, etc.)
}Performance Considerations
- The autocomplete engine is optimized for fast suggestions even with large context objects
- Suggestions are generated on-demand and cached internally
- String literal detection prevents unnecessary processing inside quoted strings
- Namespace detection is efficient and only checks relevant functions/transforms
Other implementations
- PyJEXL - A Python-based JEXL parser and evaluator.
- JexlApex - A Salesforce Apex JEXL parser and evaluator.
License
Jexl is licensed under the MIT license. Please see LICENSE.txt for full details.
Credits
Current Maintainer: Pawel Uchida-Psztyc (@jarrodek) - Modernized the library with TypeScript, enhanced type safety, and modern tooling.
Original Creator: Tom Shawver (@TomFrost) - Created the original Jexl library in 2015.
Contributors: Thanks to all the contributors who helped make the original Jexl library great.
The original Jexl was created at TechnologyAdvice in Nashville, TN.
