Expression properties
Expression properties are template-based computed fields that combine multiple properties into dynamic, formatted strings. Perfect for display names, labels, descriptions, and any scenario where you need to compose values dynamically.
What are expression properties?
An expression property uses a template string with variable interpolation to create computed values:
{
key: 'displayName',
type: 'string',
expression: '${firstName} ${lastName}',
}How it works:
Input: { firstName: 'Alice', lastName: 'Johnson' }
Output: { displayName: 'Alice Johnson' }Key features:
- Variable interpolation with
${variable} - String concatenation
- Access to all properties in the same model
- Access to referenced model properties
- Automatic updates when source values change
Evaluation rules
Expressions are text templates, not formulas:
| Rule | Behaviour |
|---|---|
| Placeholders | Each ${path} is replaced with the value at that path. Any other syntax, such as {{name}}, stays literal text |
| Result | Always a string; the declared type is not applied. There is no arithmetic, comparison or condition |
| Missing values | A path without a value becomes an empty string; a null value renders as 'null' |
| Value conversion | JavaScript string conversion: numbers as numbers (8.0 becomes '8'), arrays joined with commas ('red,blue'), json objects as '[object Object]', date values as Date.toString(), which depends on the time zone ('Wed Jan 15 2025 00:00:00 GMT+0000 (…)') |
| References | Single-valued references resolve two levels deep (${brand.name}, ${brand.country.name}); ${tags.0.name} picks an entry of a multi-valued reference of the item. One level further a reference holds only its key: ${brand.country.region.key} resolves, ${brand.country.region.name} is empty |
| Order | Expressions are evaluated in the order of properties, so declare an expression before another expression that uses it |
| Updates | Computed in init(), and recomputed on dcupl.update() for items whose values (or items they reach through single-valued references) changed. A path through a multi-valued reference (${tags.0.name}) is not recomputed when the referenced item gets a partial update; it stays stale until a full rebuild (for example after a data.set()) |
To show a date in an expression, keep it as a string property or format it before loading, for example in a loader transformer.
Basic expression
Simple variable interpolation
import { Dcupl } from '@dcupl/core';
import type { ModelDefinition } from '@dcupl/common';
const dcupl = new Dcupl();
const customerModel: ModelDefinition = {
key: 'Customer',
properties: [
{ key: 'firstName', type: 'string' },
{ key: 'lastName', type: 'string' },
// Expression property
{
key: 'fullName',
type: 'string',
expression: '${firstName} ${lastName}',
},
],
data: [
{ key: 'c1', firstName: 'Alice', lastName: 'Johnson' },
{ key: 'c2', firstName: 'Bob', lastName: 'Smith' },
],
};
dcupl.models.set(customerModel);
await dcupl.init();
const customerList = dcupl.lists.create({ modelKey: 'Customer' });
const customers = customerList.catalog.query.items();
console.log(customers[0].fullName); // 'Alice Johnson'
console.log(customers[1].fullName); // 'Bob Smith'
// Filter on expression property ('/Alice/' matches anywhere in the value)
customerList.catalog.query.addCondition({
attribute: 'fullName',
operator: 'find',
value: '/Alice/',
});
// → Alice JohnsonString concatenation
With static text
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'sku', type: 'string' },
{ key: 'price', type: 'float' },
// Product title with SKU
{
key: 'displayTitle',
type: 'string',
expression: '${name} (${sku})',
},
// Price label
{
key: 'priceLabel',
type: 'string',
expression: 'Price: $${price}',
},
],
data: [
{ key: 'p1', name: 'Laptop', sku: 'LPT-001', price: 999 },
{ key: 'p2', name: 'Mouse', sku: 'MSE-002', price: 29 },
],
};
dcupl.models.set(productModel);
await dcupl.init();
const productList = dcupl.lists.create({ modelKey: 'Product' });
const products = productList.catalog.query.items();
console.log(products[0].displayTitle); // 'Laptop (LPT-001)'
console.log(products[0].priceLabel); // 'Price: $999'Multi-line templates
A \n in the expression is a line break in the value:
const employeeModel: ModelDefinition = {
key: 'Employee',
properties: [
{ key: 'firstName', type: 'string' },
{ key: 'lastName', type: 'string' },
{ key: 'title', type: 'string' },
{ key: 'department', type: 'string' },
{ key: 'email', type: 'string' },
// Display name with title
{
key: 'displayName',
type: 'string',
expression: '${firstName} ${lastName}, ${title}',
},
// Email signature, one value per line
{
key: 'emailSignature',
type: 'string',
expression: '${firstName} ${lastName}\n${title}\n${department}\n${email}',
},
],
data: [
{
key: 'e1',
firstName: 'Alice',
lastName: 'Johnson',
title: 'Senior Engineer',
department: 'Engineering',
email: 'alice@example.com',
},
],
};
dcupl.models.set(employeeModel);
await dcupl.init();
const employeeList = dcupl.lists.create({ modelKey: 'Employee' });
const employees = employeeList.catalog.query.items();
console.log(employees[0].displayName);
// 'Alice Johnson, Senior Engineer'
console.log(employees[0].emailSignature);
// Alice Johnson
// Senior Engineer
// Engineering
// alice@example.comComplex expressions
Using reference properties
Expression properties can access properties from referenced models:
const brandModel: ModelDefinition = {
key: 'Brand',
properties: [
{ key: 'name', type: 'string' },
{ key: 'tagline', type: 'string' },
],
data: [{ key: 'b1', name: 'Apple', tagline: 'Think Different' }],
};
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'model', type: 'string' },
// Expression using reference property
{
key: 'fullTitle',
type: 'string',
expression: '${brand.name} ${name} - ${model}',
},
],
references: [{ key: 'brand', type: 'singleValued', model: 'Brand' }],
data: [{ key: 'p1', name: 'iPhone', model: '14 Pro', brand: 'b1' }],
};
dcupl.models.set(brandModel);
dcupl.models.set(productModel);
await dcupl.init();
const productList = dcupl.lists.create({ modelKey: 'Product' });
const products = productList.catalog.query.items();
console.log(products[0].fullTitle); // 'Apple iPhone - 14 Pro'The value resolves, but SDK 2.0 also records an UnknownExpressionVariable quality error for ${brand.name}, because the validator only knows the model's own properties and references. To avoid the error, derive the referenced value into a property first and use that property in the expression.
Combining derived and expression properties
Derived properties are filled before expressions, so an expression can use them:
const brandModel: ModelDefinition = {
key: 'Brand',
properties: [{ key: 'name', type: 'string' }],
data: [{ key: 'b1', name: 'Apple' }],
};
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
// Derived from the brand reference
{ key: 'brandName', type: 'string', derive: { localReference: 'brand', remoteProperty: 'name' } },
// Uses the derived property: no quality error
{ key: 'fullTitle', type: 'string', expression: '${brandName} ${name}' },
],
references: [{ key: 'brand', type: 'singleValued', model: 'Brand' }],
data: [{ key: 'p1', name: 'iPhone', brand: 'b1' }],
};
dcupl.models.set(brandModel);
dcupl.models.set(productModel);
await dcupl.init();
const productList = dcupl.lists.create({ modelKey: 'Product' });
console.log(productList.catalog.query.items()[0].fullTitle); // 'Apple iPhone'See Derived properties for the derive options.
Expressions using other expressions
An expression can use another expression property declared before it:
const softwareModel: ModelDefinition = {
key: 'Software',
properties: [
{ key: 'name', type: 'string' },
{ key: 'major', type: 'int' },
{ key: 'minor', type: 'int' },
{ key: 'patch', type: 'int' },
// Semantic version
{
key: 'version',
type: 'string',
expression: '${major}.${minor}.${patch}',
},
// Full software name: uses version, so it comes after it
{
key: 'fullName',
type: 'string',
expression: '${name} v${version}',
},
],
data: [{ key: 's1', name: 'dcupl', major: 2, minor: 5, patch: 3 }],
};
dcupl.models.set(softwareModel);
await dcupl.init();
const softwareList = dcupl.lists.create({ modelKey: 'Software' });
const software = softwareList.catalog.query.items();
console.log(software[0].version); // '2.5.3'
console.log(software[0].fullName); // 'dcupl v2.5.3'If fullName were declared before version, it would render as 'dcupl v'.
Best practices
1. Filter on expression properties like any property
You can use an expression property in addCondition without any flag. Add filter: true only to list it in catalog.filters for a filter UI. index: true has no effect on expression properties, so eq conditions on them scan the list's items.
{
key: 'fullName',
type: 'string',
filter: true, // only needed for catalog.filters
expression: '${firstName} ${lastName}',
}2. Keep templates simple
Prefer short templates over long ones like '${category.type} > ${category.name} > ${brand.name} ${name} (${sku})'. Split a long template into intermediate expressions (see Expression Too Complex).
3. Consider performance
Expressions are computed in init() and recomputed for changed items on dcupl.update(). For every item, each expression property resolves all of the model's references, not only the ones its template uses, so the cost grows with items × expression properties × references. On models with many references, keep the number of expression properties small.
4. Use for display, not logic
// ✅ Good: Display purposes
{
key: 'displayName',
type: 'string',
expression: '${firstName} ${lastName}',
}
// ❌ Bad: Don't use for calculations
{
key: 'total',
type: 'string',
expression: '${price} * ${quantity}', // Won't calculate!
}Troubleshooting
Expression not evaluating
Problem: Expression shows literal template
console.log(product.displayName); // '{{brand}} {{name}}'Solution: Write placeholders as ${property}; any other syntax, such as {{property}}, stays literal text. Put the expression in single or double quotes: in a backtick template literal, JavaScript fills in ${…} itself before dcupl sees the expression.
expression: '${brand} ${name}'; // ✅ dcupl placeholdersEmpty value in expression
Problem: A placeholder renders as an empty string
console.log(product.fullTitle); // 'Apple 'Solution: A placeholder whose path has no value becomes an empty string (a null value renders as 'null'). Check source properties exist on the item and the placeholder spells the key as the model does:
console.log(product.name); // Verify source propertyReference property not accessible
Problem: Can't access reference properties
expression: '${brand.name} ${name}'; // brand.name renders as ''Solution: A reference path resolves only when it follows single-valued references at most two levels deep (or picks an entry of a multi-valued reference, ${tags.0.name}) and the referenced item exists. One level further, only .key resolves. A key without an item is reported as a RemoteReferenceKeyNotFound quality error:
console.log(dcupl.quality.errors.summary()); // look for RemoteReferenceKeyNotFoundFor a longer path, derive the value onto an intermediate model with Derived properties and use that property.
Expression too complex
Problem: Very long expression is hard to maintain
Solution: Break into multiple expressions. Declare the intermediate expressions first: expressions are evaluated in the order of properties, and a placeholder for an expression that comes later renders as an empty string.
// Instead of one huge expression
{
key: 'description',
expression: '${a} ${b} ${c} ${d} ${e} ${f} ${g}',
}
// Create intermediate expressions
{
key: 'partOne',
expression: '${a} ${b} ${c}',
},
{
key: 'partTwo',
expression: '${d} ${e}',
},
{
key: 'description',
expression: '${partOne} - ${partTwo} ${f} ${g}',
}