Documentation / Ressources / Explications / The type system

The type system

Meridian's engine ships with no clinical types. Instead it ships a language for describing types — a small meta type-system — and three open registries that plugins fill at startup. Everything downstream (connection validity, runtime checking, editor tooltips) is derived from those descriptors. This is the same "empty core" principle that governs the rest of the platform (see Architecture).

This document explains the model and why it is shaped this way. For the exhaustive variant table, see the TypeRef reference; for the registry APIs, see Type-system registries.

TypeRef: describing a shape

Every port and every field declares its shape with a recursive TypeRef (libs/shared/src/domain/types.ts). There are eight variants:

  • primitive — a scalar (Decimal, String, Integer, Boolean, DateTime).
  • coded — a code bound to a terminology (a value-set).
  • quantity — a dimension-aware measurement, unit-checked against UCUM.
  • object — a structured datatype or resource from the registry.
  • record — a workflow-local structural object with inline field descriptors.
  • ref — a reference to an entity.
  • list — a homogeneous collection.
  • any — a flow port whose type is inferred from what it is wired to.

The point of a closed set of variants over an open set of registered vocabularies: the engine understands the eight ways a shape can be composed, while knowing nothing about which concrete objects, code systems, or dimensions exist. A plugin adds Observation or renal-stage; the engine already knows how to compare, validate, and display anything expressed with them.

Three open registries

The vocabulary lives in three registries, all empty at boot:

  • OBJECT_TYPES — descriptors for object types (datatypes, clinical resources), filled by registerType.
  • VALUE_SETS — terminologies backing coded types, filled by registerValueSet. A value-set may be external: true: its concepts are not enumerated but resolved by a terminology server through the terminology port.
  • DIMENSIONS — physical dimensions backing quantity types (a canonical unit plus conversion factors), filled by registerDimension. The common physical dimensions (mass, volume, time, concentrations…) are contributed by @posos/common.

A plugin populates these during loading, in dependency order, so a value-set or type is always registered before a node that references it.

Event payloads use the same vocabulary. A plugin declares the fields of an event with TypeRefs in contributes.events; they must resolve in the plugin's dependsOn closure, and the declaration and its example are checked when the plugin loads. An event received by POST /api/events is checked against its contract according to the instance's EVENT_CONTRACTS mode (Event ingestion API). See Plugin manifest.

What the engine derives

From these descriptors alone, three capabilities fall out for free — no per-type code:

  • Structural compatibility of connections — subtyping by fields, by value-set, and by dimension (type-system.ts). This is what lets the editor reject an incompatible wire before a run (see Two levels of validation).
  • Runtime value validation at every port boundary.
  • Field metadata exported as JSON for the UX (tooltips, dropdowns, the palette).

Dates carry a checked format

Most string primitives (String, Code, Uri, Time, Duration) are checked only for being strings. Date and DateTime also have their format checked, because a date is the one scalar that systems downstream parse rather than display: a FHIR server refuses a Communication.sent of "" or 20260611083000, and flow.wait computes its resume instant from the value. Checking the format where the value is produced turns that late refusal into a failure on the node that produced the bad value.

The rules follow what the values are used for:

  • DateTime is the FHIR dateTime. FHIR adapters read and write these values as dateTime elements. The grammar accepts the reduced precisions YYYY, YYYY-MM and YYYY-MM-DD: a condition onset known only by year is legitimate clinical data, and refusing it would fail the run that reads it. A time must carry seconds and an offset (Z or ±hh:mm), since a time without an offset means a different instant on each server.
  • Date is a full calendar day, YYYY-MM-DD. A Date port therefore never receives a partial date or a time.
  • Date.parse is not the rule. It accepts free text such as May 11, 2026, reads 01/05/2026 as January 5, and moves February 30 to March. The grammar is checked first; every accepted value remains readable by Date.parse.

An unknown date is expressed by absence, not by an empty string: a producer omits the port, and absence propagation skips whatever needed the date. The TypeRef reference lists the accepted and refused forms.

No clinical TypeScript in the core

The core hosts no clinical TypeScript interface. A plugin that wants local static typing for its own behavior code generates it from its manifest (import type declarations, erased at runtime — e.g. external-plugins/demo/generated/types.ts). The result is zero coupling: the engine depends on the descriptors, never on a plugin's compiled types. See Codegen internals for how that projection works.

97 documents5 sectionssource : /docs · généré au build