npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fhirpath-ts

v0.4.0

Published

TypeScript-native FHIRPath engine with compile-time type inference and a spec §11 static analyzer.

Readme

fhirpath-ts

A TypeScript-native FHIRPath engine for application development. It has zero runtime dependencies and includes:

  • typed results for literal expressions in plain TypeScript;
  • DTOs that transform FHIR resources into typed application data;
  • static checks through TypeScript, ESLint, a CLI linter/analyzer, and a public analyzer API;
  • official HL7 conformance tests plus tests from other FHIRPath engines;
  • Medplum compatibility;
  • currently focused in FHIR R4, the most commonly used FHIR version.

Check the playground demo or keep reading!

Quick start

import { r4 } from 'fhirpath-ts/r4'

const patient = {
  resourceType: 'Patient',
  active: true,
  name: [{ family: 'Okoro', given: ['Adaeze', 'Mina'] }],
} as const

r4.evaluate('Patient.name.given', patient) // ['Adaeze', 'Mina']
r4.first('Patient.name.family', patient) // 'Okoro'
r4.test(patient, 'Patient.active = true') // true

const family = r4.compile('Patient.name.family')
family.evaluate(patient) // ['Okoro']

Engine methods parse on demand and cache by expression text. Build a separate engine when you need different defaults:

import { FhirPathEngine } from 'fhirpath-ts'
import { r4Model } from 'fhirpath-ts/r4'

const fp = new FhirPathEngine({
  model: r4Model,
  env: { threshold: 5 },
})
fp.evaluate('%threshold + 1') // number[]; [6]
const strict = new FhirPathEngine({ model: r4Model, strict: true })
strict.evaluate('Patient.name.givenn', patient) // throws FhirPathTypeError

See API reference for all entry points, options, custom functions, DTO behavior, parse caching, and Medplum type compatibility.

Suggested usage: DTOs

For application code, prefer a view when several expressions build one row or view model. The resource type is declared once. Each field's TypeScript type is inferred from its expression, and getters or methods can handle work that is clearer in TypeScript.

import { r4 } from 'fhirpath-ts/r4'

class PatientRow extends r4.defineView('Patient') {
  id = this.column('id', { default: '' }) // string
  family = this.column("name.where(use = 'official').first().family", { default: '' }) // string
  givenNames = this.column('name.given', { collection: true }) // string[]
  active = this.criteria('active = true') // boolean

  get label(): string {
    return this.family || this.id
  }
}

const rows = PatientRow.from([patient])
rows[0] // PatientRow { id: '', family: '', givenNames: [], active: true }
rows[0]?.label // ''

PatientRow.from() accepts a Patient, an array of them, or a Bundle, and rejects anything else at compile time. Mark a column required: true when the input must carry its path; the field then has no undefined and DtoInput<typeof PatientRow> names what from() accepts.

Define DTOs and register them to call their columns from any expression. register() returns a new engine whose types know those functions:

class CodeableConceptDto extends r4.defineDto('CodeableConcept') {
  displayText = this.column('(text | coding.display.first() | coding.first().code).first()')
}

export const fp = r4.register(CodeableConceptDto)

fp.first('Condition.code.displayText()', condition) // string | undefined

class ProblemRow extends fp.defineView('Condition') {
  name = this.column('code.displayText()', { default: 'Condition' }) // string
}

This is useful for shared application vocabulary such as displayText() or isFinal().

Keep registered DTOs in one *.dto.ts module that exports the final engine. Views can live anywhere; each imports that engine and extends its defineView(). fhirpath-check imports modules matching **/*.dto.ts by default and checks each DTO against its engine. See Engines and file layout and DTO discovery.

Usage recipes

These short examples show the main application APIs. The playground demo is the best place to explore expressions interactively. A test runs every static expression in this section and checks it with the analyzer.

Read, test, and filter

r4.evaluate('Patient.name.given', patient)
r4.first('Patient.name.family', patient)
r4.test(patient, 'birthDate <= today()', { now: new Date('2026-08-04T12:00:00Z') })

const systolic = r4.filter(
  observations,
  "Observation.code.coding.exists(system = %loinc and code = '8480-6')",
  { env: { loinc: 'http://loinc.org' } },
)
const newestFirst = r4.evaluate(
  'Observation.sort(-(effective.ofType(dateTime) | issued).first())',
  systolic,
)

Check constraints

checkConstraints() evaluates constraint expressions. It does not perform full profile validation such as cardinality, bindings, or slicing.

const result = r4.checkConstraints(patient, [
  {
    key: 'pat-1',
    severity: 'error',
    human: 'Contact needs a name or telecom',
    expression: 'contact.all(name.exists() or telecom.exists())',
  },
])

result.valid // true
result.issues // []
result.toOperationOutcome() // { resourceType: 'OperationOutcome', issue: [{ severity: 'information', ... }] }

Convert quantities

Known UCUM units compare and convert automatically. Unknown units can still compare with the same unit.

r4.filter(observations, "value.ofType(Quantity) > 140 'mm[Hg]'")
r4.filter(observations, "value.ofType(Quantity).convertsToQuantity('kg')")
r4.first("Observation.value.ofType(Quantity).toQuantity('kg').value", weight)

Map status labels

choices converts codes into typed application values. A missing code becomes empty, so default also handles future or unexpected codes.

const STATUS_CHOICES = [
  { code: 'active', label: 'Active', tone: 'info' },
  { code: 'recurrence', label: 'Recurrence', tone: 'danger' },
  { code: 'resolved', label: 'Resolved', tone: 'neutral' },
] as const

const statusRows = r4.project(conditions, {
  label: {
    path: 'Condition.clinicalStatus.coding.first().code',
    choices: STATUS_CHOICES,
    pick: 'label',
    default: 'Unknown',
  },
  tone: {
    path: 'Condition.clinicalStatus.coding.first().code',
    choices: STATUS_CHOICES,
    pick: 'tone',
    default: 'neutral' as const,
  },
})

statusRows // [{ label: 'Active', tone: 'info' }, ..., { label: 'Unknown', tone: 'neutral' }]

Join related resources

Pass the related resources through env and declare their types with envTypes. Use vars to find the related item once per row. %context is the resource for the current row. With the types declared, fhirpath-check verifies every path, including the join itself.

r4.project(
  orders,
  {
    resultDate: {
      path: '(%report.effective.ofType(dateTime) | %report.issued).first()',
      default: null,
    },
    hasResult: { test: '%report.exists()' },
  },
  {
    env: { reports },
    envTypes: { reports: { type: 'DiagnosticReport', collection: true } },
    vars: { report: "%reports.where(basedOn.reference = 'ServiceRequest/' + %context.id).first()" },
    varTypes: { report: { type: 'DiagnosticReport' } },
  },
)

When the join key is not on the resource, join in TypeScript instead and keep each expression rooted in its own typed resource. Untyped values in env cannot be verified: paths through them are reported as unchecked-navigation warnings. See Static checking.

Follow references in a Bundle

resolve() follows contained references and references to another Bundle entry. Start at Bundle so the Bundle remains available as the lookup scope.

r4.evaluate(
  'Bundle.entry.resource.ofType(Observation).subject.resolve().name.family',
  searchset,
)

Walk nested structures

repeat(item) follows every nested item collection and returns the items at all depths, so this reads each questionnaire item's linkId.

r4.evaluate('Questionnaire.repeat(item).linkId', questionnaire)

Deterministic tests and debugging

now fixes the evaluation clock. trace() sends values to the sink you provide and does not log by itself.

r4.test(patient, 'birthDate <= today()', {
  now: new Date('2026-08-04T12:00:00Z'),
})

r4.evaluate("Patient.name.trace('names').given", patient, {
  trace: (name, values) => debugSink(name, values),
})

Important gotchas

  • FHIRPath always evaluates collections. Use first() when application code expects one optional value.
  • Unknown elements evaluate to an empty collection, as they do in other engines. Use static checking, or set strict: true on an engine or evaluation call, to catch misspellings before data is read.
  • Use choice stems such as Observation.value, not JSON keys such as valueQuantity. The analyzer and strict evaluation report the latter as an error; lenient evaluation returns an empty collection.
  • Function names and arguments are strict: an unknown function, wrong arity or type, or undefined %variable is an error. See the runtime error table.
  • A bare search Bundle is treated as its entry resources for application helpers. Start an expression at Bundle to address the Bundle itself, or wrap it in an array to force one-resource treatment.
  • Inside where(), the focus is the item being scanned. Use %context or a %var when a join condition needs the outer resource.
  • env contains plain host values. vars contains FHIRPath expressions evaluated against the input and keeps FHIRPath type information.

Static checking

Literal expressions use a bounded type-level parser that follows the runtime grammar and built-in function rules. It infers result and input types without a compiler plugin:

const given = r4.compile('Patient.name.given')
given.evaluate(patient) // string[]; the input must be a Patient

Type inference stays conservative. Malformed, over-budget, dynamically widened, or deliberately opaque expressions become unknown[] instead of producing an incorrect type. Literal host values are inferred automatically. Static envTypes and varTypes declarations cover ambiguous or widened data; see type context declarations. String literals and the branches of iif() keep their text, so iif(active, 'open', 'closed') infers ('open' | 'closed')[]. The type-level scanner budget is 128 tokens and 512 visited source characters. Type inference computes a safe TypeScript type; it does not validate the expression. ESLint and the CLI run the analyzer to report expression errors.

Static checking has three layers:

  1. TypeScript infers literal expressions at compile time.
  2. fhirpath-ts/eslint checks expression literals while linting.
  3. fhirpath-check runs the same analyzer without requiring ESLint and can load exported DTOs for a complete DTO check.

The CLI imports exported DTOs and records engines constructed by their modules. This checks runtime context, registered functions, and cross-DTO calls that TypeScript and source-only ESLint cannot see.

The analyzer is also public for editors, tests, and other tools. It follows the FHIRPath §11 rules and is tested against the official valid and invalid cases. See Static checking for configuration, supported call sites, DTO discovery, and cases where source-only checks stay quiet.

Conformance

The engine passes every non-skipped case in the vendored official R4 and R5 FHIRPath suites. It also runs the fhirpath.js and fhirpath-py corpora. Each skip and intentional difference has a checked manifest entry with its reason.

Property tests cover parser round trips, decimal arithmetic, and temporal comparisons. Generated expressions are also compared with fhirpath.js. A weekly job checks for changes in the upstream official suite (FHIR/fhir-test-cases).

See Conformance for counts, skip categories, phase checks, reference-suite results, and test maintenance. See Engine comparison for the feature and correctness comparison with other implementations.

Licensing and attribution

Package code is licensed under Apache-2.0. Third-party material and full license texts are collected in THIRD-PARTY-NOTICES.md.

  • The parser structure is adapted from Medplum (Apache-2.0).
  • Official FHIRPath tests come from FHIR/fhir-test-cases (Apache-2.0 and FHIR CC0 content).
  • Reference tests come from HL7/fhirpath.js (NLM BSD-style) and fhirpath-py (MIT).
  • Additional cases come from fhirpath-rs (Apache-2.0) and Medplum.
  • R4 model data is generated from HL7 FHIR R4 StructureDefinitions (CC0) in @medplum/definitions. FHIR® is a registered trademark of HL7.

Development

pnpm typecheck
pnpm test
pnpm lint
pnpm check:fhirpath
pnpm check:inference
pnpm check:type-perf
pnpm coverage
pnpm build
pnpm check:package

Run the demo separately:

cd demo
npm install
npm run dev

After changing the public API, run npm run generate:dts in demo/ to refresh the Monaco declarations. See AGENTS.md for repository decisions that are easy to break during maintenance.

Gaps and deferred features

The package currently ships only an R4 model. The engine is synchronous. Tagged templates remain untyped because TypeScript does not preserve their literal type (TypeScript #33304); use fhirpath('...') or compile('...') for inference in strings. Regex evaluation of user-authored expressions needs the security setup below.

These features are deferred and fail with a clear error today:

| Feature | What it needs | | --- | --- | | memberOf(), subsumes(), subsumedBy(), %terminologies | An async TerminologyProvider | | External resolve() | The same async evaluation path | | conformsTo() beyond base StructureDefinitions | Profile-aware validation | | slice(), elementDefinition(), checkModifiers() | Profile definitions in the model | | weight() | Code-system itemWeight lookups | | %factory | Demand for the current R5 draft API | | CDA mode | A CDA ModelProvider | | Full UCUM | A full UCUM implementation behind the current interface | | R5 model package | Generated R5 definitions and types |

Security guidelines

Expression trust

Parser depth, tokenization, decimal and UCUM exponents, and property navigation have limits suitable for untrusted input. Regular expressions need one extra step. By default, matches(), matchesFull(), and replaceMatches() use the host RegExp, so a pattern with catastrophic backtracking can block the event loop.

The analyzer warns about backtracking-prone literal patterns. If users can write expressions, also provide a linear-time regular expression engine, such as an RE2 binding, through EvaluateOptions.regex. Review or sandbox user expressions as appropriate for the application.

Narrative checking

htmlChecks() checks the FHIR narrative rules: well-formed XHTML in one div, only the elements and attributes FHIR allows, no scripts (including javascript: links), and some non-whitespace text or an image. An xhtml element, such as text.div, must be a complete narrative div. A string, including code, id, and markdown, is checked as the content of a div. Only a model, such as r4Model, types text.div as xhtml; without one it is a string and is checked as div content. Other items and collections give an empty result.

The FHIR rules are not an HTML sanitizer, so a true result does not mean a narrative is safe to insert into a page. If your application renders narrative as HTML, sanitize it before rendering. We strongly recommend also passing a sanitizer to the engine, so htmlChecks() returns false for any narrative the sanitizer would change:

import createDOMPurify from 'dompurify'
import { domPurifySanitizer, FhirPathEngine } from 'fhirpath-ts'
import { r4Model } from 'fhirpath-ts/r4'

// In a browser, pass `window`. In Node, pass a DOM such as `new JSDOM('').window`.
const DOMPurify = createDOMPurify(window)
const fp = new FhirPathEngine({ model: r4Model, narrativeSanitizer: domPurifySanitizer(DOMPurify) })

DOMPurify is not a dependency of this package; install it yourself. Any object with an accepts(xhtml) method works as a narrativeSanitizer. The adapter configures DOMPurify to keep every element, attribute, and comment the FHIR rules allow. DOMPurify still removes urn: links; to keep them, pass a config with ALLOWED_URI_REGEXP as the second argument of domPurifySanitizer(). Its settings override the adapter's defaults one by one.

PHI and tracing

trace() does nothing unless a trace sink is provided. Traced values may contain patient data. Never send PHI values to console output or production logs; use record identifiers instead.