Skip to main content

FHIRPath Expression Builder

@fhir-dsl/fhirpath provides a type-safe FHIRPath expression builder for TypeScript. Navigate FHIR resources with property access, build FHIRPath strings, and evaluate expressions -- all with full autocomplete and compile-time type safety.

Installation​

npm install @fhir-dsl/fhirpath

Quick Start​

import { fhirpath } from "@fhir-dsl/fhirpath";
import type { Patient } from "./fhir/r4";

// Type-safe path navigation with autocomplete at every step
const expr = fhirpath<Patient>("Patient").name.family;
expr.compile(); // "Patient.name.family"
expr.evaluate(somePatient); // ["Smith", "Doe"]

Expression Predicates​

Use callback expressions with $this for filtering, existence checks, and projections:

// where() with expression predicate
fhirpath<Patient>("Patient")
.name.where($this => $this.use.eq("official"))
.family.compile();
// "Patient.name.where($this.use = 'official').family"

// Backward compatible simple form
fhirpath<Patient>("Patient")
.name.where("use", "official")
.family.compile();
// "Patient.name.where(use = 'official').family"

// exists() with criteria
fhirpath<Patient>("Patient")
.name.exists($this => $this.use.eq("official"))
.compile();
// "Patient.name.exists($this.use = 'official')"

// all() -- true if all items match
fhirpath<Patient>("Patient")
.name.all($this => $this.family.exists())
.compile();
// "Patient.name.all($this.family.exists())"

// select() -- project each element
fhirpath<Patient>("Patient")
.name.select($this => $this.family)
.compile();
// "Patient.name.select($this.family)"

Comparison & Boolean Operators​

Available inside expression predicates:

// Comparison operators: eq, neq, lt, gt, lte, gte
$this.value.gt(100) // $this.value > 100
$this.status.neq("draft") // $this.status != 'draft'

// Boolean operators: and, or, xor, not, implies
$this.use.eq("official").and($this.family.exists())
$this.active.not()

Collection Operations​

Subsetting​

fp.name.first() // First element
fp.name.last() // Last element
fp.name.single() // Exactly one element (throws if > 1)
fp.name.tail() // All but first
fp.name.skip(2) // Skip first 2
fp.name.take(3) // Take first 3

Existence & Filtering​

fp.name.count() // Number of elements
fp.name.exists() // Non-empty?
fp.name.empty() // Empty?
fp.name.distinct() // Remove duplicates
fp.name.isDistinct() // All values unique?
fp.name.allTrue() // All items are true?
fp.name.anyTrue() // Any item is true?
fp.name.allFalse() // All items are false?
fp.name.anyFalse() // Any item is false?

Combining​

fp.name.union(otherExpr) // Merge + deduplicate
fp.name.combine(otherExpr) // Merge with duplicates
fp.name.intersect(otherExpr) // Elements in both
fp.name.exclude(otherExpr) // Elements not in other

String Functions​

fp.name.family.upper() // Uppercase
fp.name.family.lower() // Lowercase
fp.name.family.length() // String length
fp.name.family.startsWith("Sm") // Starts with prefix?
fp.name.family.endsWith("th") // Ends with suffix?
fp.name.family.contains("mit") // Contains substring?
fp.name.family.indexOf("mi") // Index of substring
fp.name.family.substring(0, 3) // Extract substring
fp.name.family.replace("Smith", "Jones") // Replace text
fp.name.family.matches("^S.*h$") // Regex match
fp.name.family.replaceMatches("[aeiou]", "*") // Regex replace
fp.name.family.toChars() // Split to characters

Math Functions​

fp.valueQuantity.value.abs() // Absolute value
fp.valueQuantity.value.ceiling() // Round up
fp.valueQuantity.value.floor() // Round down
fp.valueQuantity.value.round(2) // Round to precision
fp.valueQuantity.value.truncate() // Truncate to integer
fp.valueQuantity.value.sqrt() // Square root
fp.valueQuantity.value.power(3) // Raise to power
fp.valueQuantity.value.ln() // Natural logarithm
fp.valueQuantity.value.log(10) // Log with base
fp.valueQuantity.value.exp() // e^x

Conversion Functions​

fp.active.toBoolean() // Convert to boolean
fp.active.toInteger() // Convert to integer
fp.active.toDecimal() // Convert to decimal
fp.active.toFhirString() // Convert to string (FHIRPath toString)
fp.birthDate.toDate() // Convert to date
fp.birthDate.toDateTime() // Convert to dateTime
fp.active.convertsToBoolean() // Can convert to boolean?
fp.active.convertsToInteger() // Can convert to integer?
// ... and more convertsTo* variants
info

toFhirString() maps to FHIRPath's toString() function. The JS toString() method returns the compiled path string for serialization.

Type Narrowing with ofType()​

Filter collections by FHIR type -- critical for polymorphic value[x] fields:

fhirpath<Observation>("Observation")
.value.ofType("Quantity").value
.compile();
// "Observation.value.ofType(Quantity).value"

The FhirTypeMap interface is extensible via declaration merging to add your own types.

Utility Functions​

fp.name.trace("debug") // Debug trace (returns input unchanged)
fp.now() // Current dateTime
fp.today() // Current date
fp.timeOfDay() // Current time
fp.name.children() // Immediate children
fp.name.descendants() // All descendants

Conditional: iif()​

fp.name.first().iif(
$this => $this.use.eq("official"), // criterion
$this => $this.family, // true result
$this => $this.given, // otherwise result
).compile();
// "Patient.name.first().iif($this.use = 'official', $this.family, $this.given)"

Recursive Navigation: repeat()​

fhirpath<Organization>("Organization")
.repeat($this => $this.partOf)
.compile();
// "Organization.repeat($this.partOf)"

FHIRPath Spec Coverage​

@fhir-dsl/fhirpath covers the pragmatic subset of the official FHIRPath specification that FHIR invariants and common navigation actually use, plus FHIR-specific extensions (UCUM-aware Quantity, terminology hooks, write-back).

CategoryFunctionsStatus
NavigationProperty access, children, descendantsImplemented
Filteringwhere, select, repeat, ofTypeImplemented
Existenceexists, all, allTrue/anyTrue/allFalse/anyFalse, count, empty, distinct, isDistinctImplemented
Subsettingfirst, last, single, tail, skip, take, intersect, excludeImplemented
Combiningunion, combineImplemented
StringindexOf, substring, startsWith, endsWith, contains, upper, lower, replace, matches, replaceMatches, length, toCharsImplemented
Mathabs, ceiling, exp, floor, ln, log, power, round, sqrt, truncateImplemented
Arithmetic.add(), .sub(), .mul(), .div(), .divTrunc() (div), .mod(), .concat() (&) — chained builder methods that compile to the spec operatorsImplemented
ConversiontoBoolean, toInteger, toDecimal, toString, toDate, toDateTime, toTime, toQuantity, convertsTo*Implemented
Operators=, !=, <, >, <=, >=, and, or, xor, not, implies, is, asImplemented
Aggregateaggregate(fn, init?), plus shorthands sum(), min(), max(), avg()Implemented
Utilitytrace, now, today, timeOfDay, iifImplemented
FHIR — write-backsetValue(resource, value), createPatch(resource, value) (RFC 6902)Implemented (v0.53.0)
FHIR — quantityUCUM-aware equality + ordering on same-dimension Quantity (5 'mg' = 0.005 'g' is true)Implemented (v1.1.0)
FHIR — referencesresolve() walks the rootResource Bundle; EvalOptions.resolveReference hook for non-Bundle framesImplemented (v1.1.0)
FHIR — terminologyconformsTo(profile), memberOf(valueSet), subsumes(other), subsumedBy(other) — compile to spec strings, evaluate via EvalOptions.terminology resolverImplemented (v1.1.0)
FHIR — invariantscompileInvariant() + validateInvariants() against ElementDefinition.constraint[*].expressionImplemented
FHIR — extensionsextension(url) (sugar for .extension.where(url = '<url>'))Implemented
Equivalence~ (equivalence), !~ (not equivalence)Not yet — track at spec-compliance.test.ts FP-EQ-003
UCUM — special unitsOffset units (Celsius, Fahrenheit) and logarithmic units (pH, decibel)Not supported — parser throws UcumError instead of producing silent wrong answers; normalise upstream
UCUM — multi-/Multi-factor compound expressions like mol/(L.s)Not yet — single-/ covers every healthcare case in current scope