Documentation / Développement / Référence / TypeRef

TypeRef

TypeRef is the discriminated union that describes the type of every node port and every object field in Meridian. It is defined in libs/shared/src/domain/types.ts and re-exported to plugins from @meridian/shared/domain/types. A TypeRef value is pure data (JSON-serializable) — the same descriptor is written in a plugin's plugin.yaml, carried at runtime on PortDef/ConfigFieldDef/FieldDescriptor, projected to TypeScript by codegen, and consumed by the static-validation and runtime-validation engines.

The union

export type TypeRef =
  | { kind: "primitive"; name: PrimitiveName }
  | { kind: "coded"; valueSet: string }
  | { kind: "quantity"; dimension?: Dimension }
  | { kind: "ref"; entity: string }
  | { kind: "object"; name: string }
  | { kind: "list"; of: TypeRef }
  | { kind: "any" };

list is the only recursive case (of: TypeRef); every other kind is a leaf. Dimension is the closed string union from libs/shared/src/domain/dimensions.ts.

The vocabulary is layered, per the module's header comment:

Layer Kind(s) Backed by
0 — primitives primitive the closed PrimitiveName union (no registry)
1 — coded coded the VALUE_SETS registry (domain/value-sets.ts)
2 — measures & structures quantity, object, ref DIMENSIONS (domain/dimensions.ts), OBJECT_TYPES (domain/object-types.ts), and CONTEXT_KINDS (domain/context-kinds.ts — a ref entity is the entity of a contributed context kind)

any and list sit outside the layering: any is a wildcard (see Structural assignability); list wraps any other TypeRef, including another list.

Kinds

kind Fields (besides kind) Meaning Generated TypeScript Runtime value checked by validateValue
primitive name: PrimitiveName A language-level scalar. per PrimitiveName, see table below per PrimitiveName, see below
coded valueSet: string A code bound to a value-set (VALUE_SETS registry id). literal union of the value-set's codes, or string if the value-set is external, has no enumerated codes, or isn't in the plugin's dependsOn closure typeof value === "string"; if the value-set is resolvable internally (not external), the code must also be a member of it
quantity dimension?: Dimension A dimension-aware measure (UCUM-style unit). Omitting dimension accepts any measure. { value: number; unit: string; system?: string } an object with value: number (non-NaN) and unit: string; if dimension is set, unit must belong to that dimension (dimensionOfUnit)
object name: string A datatype or resource registered in OBJECT_TYPES (by name). the generated interface for that name (imported from the owning plugin's package if not self-owned) an object satisfying every field of the registered TypeDescriptor, recursively — see Backing registries
ref entity: string A reference to the entity of a contributed context kind (e.g. Patient for the patient kind), by id only — open, resolved against the CONTEXT_KINDS registry. { id: string } an object with id: string
list of: TypeRef A homogeneous list of another TypeRef. T[], where T is the mapping of of an array whose every element satisfies of (index reported on failure)
any A flow port whose type is inferred from wiring (used by engine primitives such as flow.switch/flow.map/flow.guard, and transform.*). unknown always valid

Primitive names

PrimitiveName is a closed, FHIR-aligned enum — the only kind with no backing registry:

PrimitiveName Generated TypeScript (PRIMITIVE_TS) Runtime check (validateValue) Notes
String string typeof value === "string" free text
Number number typeof value === "number" && !Number.isNaN(value) retained as a back-compat alias of Decimal (see code comment in domain/types.ts)
Decimal number typeof value === "number" && !Number.isNaN(value) real value
Integer number typeof value === "number" && Number.isInteger(value)
Boolean boolean typeof value === "boolean"
Date string typeof value === "string" date without time
DateTime string typeof value === "string" ISO 8601 instant
Time string typeof value === "string"
Duration string typeof value === "string" ISO 8601 duration or seconds
Code string typeof value === "string" a raw token, before terminology binding
Uri string typeof value === "string"

Codegen looks the name up in a fixed PRIMITIVE_TS map (libs/plugin-cli/src/codegen.ts); every PrimitiveName has an entry, so the map's ?? "unknown" fallback is never exercised in practice.

Construction helpers — the t namespace

domain/types.ts exports a t object of ready-made TypeRef values and factories, used throughout the core catalogue (engine/catalog-metadata.ts) and available to plugin code:

Member Value / signature
t.string, t.number, t.decimal, t.integer, t.boolean, t.date, t.dateTime, t.time, t.duration, t.code, t.uri { kind: "primitive", name: <PrimitiveName> } for the matching name
t.ref(entity: string): TypeRef { kind: "ref", entity }
t.coded(valueSet: string): TypeRef { kind: "coded", valueSet }
t.quantity(dimension?: Dimension): TypeRef { kind: "quantity", dimension }
t.obj(name: string): TypeRef { kind: "object", name }
t.list(of: TypeRef): TypeRef { kind: "list", of }
t.any { kind: "any" }

Human-readable labels — typeLabel

typeLabel(type: TypeRef): string (domain/types.ts) renders a TypeRef for logs, error messages, and the catalogue UX:

kind Output
primitive name (e.g. Decimal)
coded Code<valueSet>
quantity Quantity<dimension> if dimension is set, else Quantity
ref entity `Ref` (e.g. PatientRef)
object name
list List<typeLabel(of)>, recursively
any Any

Structural assignability — isAssignable

isAssignable(from: TypeRef, to: TypeRef): boolean (libs/shared/src/engine/type-system.ts) decides whether a connection's source output type (from) may feed a target input type (to). It is evaluated in this order:

  1. any wildcard. If either from.kind or to.kind is any, the connection is allowed.
  2. codedprimitive widening. A coded output may feed a primitive input only if the target is Code or String.
  3. Otherwise from.kind must equal to.kind; the rule then depends on the kind:
kind Rule
primitive Same name → allowed. Otherwise: the numeric family (Number, Decimal, Integer) is mutually assignable except into Integer — only Integer → Integer is allowed for that target, so Number/Decimal cannot feed an Integer port. The free-text family (String, Code, Uri) is fully mutually assignable. Any string-represented primitive (String, Code, Uri, Date, DateTime, Time, Duration) may additionally widen into a String target.
coded Allowed only when from.valueSet === to.valueSet — exact match. Hierarchical subsumption (subsumes() in domain/value-sets.ts) is not consulted here; it stays intra-value-set and serves other consumers, not connection compatibility.
quantity If to.dimension is undefined, any quantity is accepted. Otherwise from.dimension must equal to.dimension exactly — no implicit unit conversion at this level (that is the contributes.conversions mechanism, powering the transform.convert node).
ref Allowed only when from.entity === to.entity.
list Allowed when isAssignable(from.of, to.of) — recursive, element-wise.
object Allowed when from.name === to.name, or by structural subtyping: every required field of to (per OBJECT_TYPES) must exist in from's fields with a recursively assignable type. A to with no known fields (unresolved type) is never satisfied this way.

Runtime validation — validateValue

validateValue(type: TypeRef, value: unknown): { ok: boolean; error?: string } (engine/type-system.ts) checks a concrete value against a TypeRef at port boundaries (trigger payload → outputs, node outputs → downstream inputs). The per-kind checks are listed in Kinds and Primitive names above; list and object recurse and report the first failing element/field (élément [i] : …, TypeName.field : …).

Example values — exampleValue

exampleValue(type: TypeRef): unknown (engine/type-system.ts) produces a deterministic value conforming to a TypeRef, used by the mock agent runner and default-value generation:

kind Example produced
primitive 0 (numeric names), true (Boolean), a fixed ISO string for Date/DateTime/Time/Duration (2026-06-08, 2026-06-08T00:00:00.000Z, 00:00:00, PT0S), "" otherwise
coded the first concept's code in the value-set, or "" if it has none
quantity { value: 0, unit }, where unit is the dimension's canonical unit (or "1" with no dimension)
ref { id: "example" }
list []
object an object populated with exampleValue for every required field only
any null

YAML encoding (plugin manifest)

plugin.yaml encodes TypeRef values with the exact same shape, validated by the recursive TypeRefSchema (Zod) in libs/shared/src/plugin/manifest.ts:

const TypeRefSchema: z.ZodType = z.lazy(() =>
  z.union([
    z.object({ kind: z.literal("primitive"), name: PrimitiveName }),
    z.object({ kind: z.literal("coded"), valueSet: z.string() }),
    z.object({ kind: z.literal("quantity"), dimension: z.string().optional() }),
    z.object({ kind: z.literal("ref"), entity: z.enum(["Patient", "Encounter", "Order", "Document"]) }),
    z.object({ kind: z.literal("object"), name: z.string() }),
    z.object({ kind: z.literal("list"), of: TypeRefSchema }),
    z.object({ kind: z.literal("any") }),
  ]),
);

One asymmetry: the manifest schema types quantity.dimension as a bare z.string().optional(), while the internal TypeRef types it as the closed Dimension union. A manifest is free to name any dimension string; it only becomes meaningful once that name resolves in the DIMENSIONS registry (see below).

TypeRef values appear in a manifest wherever a type field is expected: contributes.types[].fields[].type, contributes.nodes[].inputs[].type / outputs[].type, and contributes.nodes[].config[].type. The host applies a fixed contribution order — value-sets → dimensions → types → conversions → nodes → adapters → agents → eventSources → eventBindings — so any coded/quantity/object reference inside a types or nodes contribution resolves against a registry already populated earlier in the same load pass. See Plugin manifest.

Backing registries

coded, quantity, object, and ref resolve names against open, in-memory registries. All of them are populated by plugins at load time; none is exposed for direct mutation outside their registration function.

Registry File Core-seeded? Registration function Collision behavior
OBJECT_TYPES (object) domain/object-types.ts No — starts empty; the core contributes no domain type registerType(name, descriptor) Throws if name is already registered with a different descriptor
VALUE_SETS (coded) domain/value-sets.ts No — starts empty registerValueSet(def) Throws if def.id is already registered with a different definition
DIMENSIONS (quantity) domain/dimensions.ts Yes — pre-seeded with 10 generic physical dimensions (dimensionless, fraction, mass, volume, time, amount-of-substance, mass-concentration, molar-concentration, pressure, temperature) registerDimension(name, def) Silently overwrites — no collision guard
CONTEXT_KINDS (ref) domain/context-kinds.ts No — starts empty; the core contributes no context kind registerContextKind(descriptor) Throws if name is already registered with a different descriptor

The Dimension TS union additionally lists filtration-rate, which is not among the core-seeded entries: it is declared in the shared vocabulary but only becomes usable once something (typically a clinical plugin) calls registerDimension("filtration-rate", …).

getType/hasValueSet/dimensionOfUnit (and friends) are the read-side accessors isAssignable, validateValue, and exampleValue use to resolve a TypeRef against these registries. See Type-system registries and Add vocabulary and types.

Ambient context (same module)

domain/types.ts also defines ContextKey (an open string — a context-kind name) and ContextEnvelope (Record<string, { id: string } | undefined>, one key per established kind) — the ambient context a workflow run carries, distinct from TypeRef's ref kind (which types a single port value, not the run's context). Node metadata references ContextKey in its context/establishes fields. See Workflow DSL and Core primitives.

  • Plugin manifest — the full plugin.yaml schema, including where TypeRef appears.
  • Generate types and Codegen internals — how TypeRef is projected to TypeScript.
  • Add vocabulary and types — contributing value-sets, dimensions, and types from a plugin.
  • Type-system registries — OBJECT_TYPES, VALUE_SETS, DIMENSIONS, CONTEXT_KINDS in depth.
  • Static validation engine — how isAssignable is used to check a whole workflow.
  • The type system — the conceptual model behind the layered vocabulary.
  • Port contracts — how TypeRef shows up on PortDef/ConfigFieldDef.
  • MapExpr — the separate grammar for projecting raw records into typed event fields.
75 documents12 sectionssource : /docs · généré au build