# 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:

```typescript
{
  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](/sdk/api/loader#transformers).

## Basic expression

### Simple variable interpolation

```typescript
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 Johnson
```

## String concatenation

### With static text

```typescript
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:

```typescript
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.com
```

## Complex expressions

### Using reference properties

Expression properties can access properties from referenced models:

```typescript
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:

```typescript
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](/sdk/advanced/derived-properties) for the derive options.

### Expressions using other expressions

An expression can use another expression property declared before it:

```typescript
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.

```typescript
{
  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](#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

```typescript
// ✅ 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

```typescript
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.

```typescript
expression: '${brand} ${name}'; // ✅ dcupl placeholders
```

### Empty value in expression

**Problem:** A placeholder renders as an empty string

```typescript
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:

```typescript
console.log(product.name); // Verify source property
```

### Reference property not accessible

**Problem:** Can't access reference properties

```typescript
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:

```typescript
console.log(dcupl.quality.errors.summary()); // look for RemoteReferenceKeyNotFound
```

For a longer path, derive the value onto an intermediate model with [Derived properties](/sdk/advanced/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.

```typescript
// 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}',
}
```

## Next steps

- [Derived properties](/sdk/advanced/derived-properties) — Compute values from references
- [Models](/sdk/core-concepts/models) — Properties and references configuration
- [Quality & validation](/sdk/advanced/quality-validation) — Validate expressions
- [Performance](/sdk/advanced/performance) — Optimization strategies
