# How the Expression System Defines Style Property Specifications in GeoLibre

> Discover how GeoLibre's expression system defines style property specifications using the propertySpecFor helper for data-driven styling and zoom/feature parameter validation.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: internals
- Published: 2026-08-04

---

**The expression system in [`packages/core/src/expressions.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/expressions.ts) builds a minimal style property specification via the `propertySpecFor` helper, which declares the expected result type and marks the property as data-driven with `["zoom", "feature"]` parameters for `createExpression` validation.**

MapLibre GL style expressions power dynamic styling in modern web maps, but enforcing type safety—ensuring a filter returns boolean, a color property returns a color—requires bridging GeoLibre's UI with MapLibre's internal style-spec compiler. The GeoLibre repository solves this through a carefully constructed property specification object that mirrors MapLibre's unexported `StylePropertySpecification` contract without directly importing it.

## The Role of `propertySpecFor` in Type Enforcement

Type validation in GeoLibre's expression system hinges on a single factory function: `propertySpecFor`. Located at [lines 222–228 of [`packages/core/src/expressions.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/expressions.ts)](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/expressions.ts#L222-L228), this helper constructs a specification object that tells MapLibre's `createExpression` exactly what result type to expect.

```typescript
function propertySpecFor(expectedType: ExpressionExpectedType) {
  return {
    type: expectedType,
    "property-type": "data-driven",
    expression: { parameters: ["zoom", "feature"] },
  } as unknown as NonNullable<Parameters<typeof createExpression>[2]>;
}

```

The function accepts an `ExpressionExpectedType`—one of `"boolean"`, `"color"`, `"string"`, or `"number"`—and returns an object with three critical properties:

- **type**: The enforced result type, passed directly from the caller's `expectedType` option.
- **property-type**: Hardcoded to `"data-driven"`, allowing the expression to reference feature attributes and zoom levels.
- **expression.parameters**: A fixed array `["zoom", "feature"]` matching MapLibre's standard expression parameters.

## Why the Double Cast to `unknown` Is Necessary

The return type `as unknown as NonNullable<Parameters<typeof createExpression>[2]>` reveals an architectural constraint: MapLibre's `StylePropertySpecification` type is **unexported** from `@maplibre/maplibre-gl-style-spec`. Rather than attempting to import a private type or redeclare the full interface, GeoLibre uses a structural shape that satisfies the compiler through type assertion.

This approach carries maintenance implications. The comment preceding `propertySpecFor` explicitly warns that any upgrade to the `@maplibre` dependency requires manual verification that the spec object still aligns with `createExpression`'s expectations. The test suite in [`tests/expressions.test.ts`](https://github.com/opengeos/GeoLibre/blob/main/tests/expressions.test.ts) provides regression coverage for this contract.

## Integrating the Spec into Expression Compilation

The `propertySpecFor` helper is invoked conditionally during expression compilation. At [lines 298–301](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/expressions.ts#L298-L301), the `compileMapExpression` function assembles the final validation call:

```typescript
const compiled = createExpression(
  substituted,
  EXPRESSION_ROOT_KEY,
  options.expectedType ? propertySpecFor(options.expectedType) : undefined,
);

```

This code path demonstrates the system's flexibility:

- **Without `expectedType`**: The third argument is `undefined`, and `createExpression` performs only syntactic validation, accepting any result type.
- **With `expectedType`**: The constructed spec forces type-checking; an expression like `["+", 1, 2]` fails validation when `expectedType: "boolean"` is requested.

## Practical Usage: Validating Expressions with Type Constraints

The public API `validateMapExpression` exposes this functionality to UI components and rule builders. Import from `@geolibre/core` and enforce specific return types:

```typescript
import { validateMapExpression } from "@geolibre/core/src/expressions";

// Enforce boolean for layer filters
const filterResult = validateMapExpression(
  '["==", ["get", "status"], "active"]',
  { expectedType: "boolean" }
);

console.log(filterResult.ok);     // true if expression compiles to boolean
console.log(filterResult.error);  // undefined, or descriptive error message

```

Color expressions for fill or line styling follow the same pattern:

```typescript
const colorResult = validateMapExpression(
  '["interpolate", ["linear"], ["zoom"], 0, "#ff0000", 10, "#0000ff"]',
  { expectedType: "color" }
);

```

When `expectedType` is omitted, the expression compiles without result-type constraints, useful for generic expression previews where the consuming context is unknown.

## How Data-Driven Parameters Enable Dynamic Styling

The fixed `parameters: ["zoom", "feature"]` declaration in `propertySpecFor` is not arbitrary. It matches MapLibre's expression evaluation context exactly:

| Parameter | Purpose |
|-----------|---------|
| `zoom` | Access to current map zoom level via `["zoom"]` expressions |
| `feature` | Access to feature properties via `["get", "propertyName"]` |

This configuration allows expressions like `["interpolate", ["linear"], ["zoom"], 0, 1, 10, 5]` for zoom-dependent width, or `["==", ["get", "type"], "highway"]` for feature-driven filtering. Without `"data-driven"` and these parameters, the expression compiler would reject inputs referencing zoom or feature data.

## Summary

- **`propertySpecFor`** constructs a minimal MapLibre-compatible style property specification that enforces expected result types while preserving data-driven capabilities.
- The spec is passed to **`createExpression`** only when **`options.expectedType`** is provided, enabling strict validation without breaking untyped use cases.
- The **double cast to `unknown`** works around MapLibre's unexported `StylePropertySpecification` type, requiring manual verification on dependency upgrades.
- **Fixed parameters `["zoom", "feature"]`** ensure expressions can reference both map state and feature attributes, matching standard MapLibre behavior.

## Frequently Asked Questions

### What happens if I omit `expectedType` when validating an expression?

The expression compiles without type constraints. `createExpression` receives `undefined` as its third argument and performs only syntactic validation, accepting any result type. This is appropriate for generic expression builders where the consuming context hasn't been determined yet.

### Why can't GeoLibre import `StylePropertySpecification` directly from MapLibre?

The type is **unexported** from `@maplibre/maplibre-gl-style-spec`. GeoLibre's maintainers chose structural typing over forking or redeclaring the full interface, accepting the maintenance burden of type assertions in exchange for compatibility across MapLibre versions.

### Which expression result types can `propertySpecFor` enforce?

The `ExpressionExpectedType` union supports `"boolean"` for filters, `"color"` for visual properties, `"string"` for text and categorical values, and `"number"` for numeric computations. Passing any other value would fail TypeScript compilation.

### Where does GeoLibre test this type enforcement logic?

The test suite in [[`tests/expressions.test.ts`](https://github.com/opengeos/GeoLibre/blob/main/tests/expressions.test.ts)](https://github.com/opengeos/GeoLibre/blob/main/tests/expressions.test.ts) covers validation success and failure cases for each expected type, ensuring that `propertySpecFor`'s structural contract remains valid through dependency updates.