How the Expression System Defines Style Property Specifications in GeoLibre

The expression system in 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#L222-L228), this helper constructs a specification object that tells MapLibre's createExpression exactly what result type to expect.

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 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, the compileMapExpression function assembles the final validation call:

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:

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:

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) covers validation success and failure cases for each expected type, ensuring that propertySpecFor's structural contract remains valid through dependency updates.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →