A TypeScript implementation of FHIRPath, the path-based navigation and extraction language for FHIR (Fast Healthcare Interoperability Resources).
npm install @atomic-ehr/fhirpath
# or
bun add @atomic-ehr/fhirpathimport fhirpath from '@atomic-ehr/fhirpath';
const patient = {
name: [
{ given: ['John', 'James'], family: 'Doe' },
{ given: ['Johnny'], family: 'Doe' }
],
birthDate: '1990-01-01'
};
// Simple evaluation (async)
const givenNames = await fhirpath.evaluate('name.given', patient);
console.log(givenNames); // ['John', 'James', 'Johnny']
// With filtering
const officialName = await fhirpath.evaluate('name.where(use = \'official\').given', patient);
// Arithmetic
const age = await fhirpath.evaluate('today().year() - birthDate.toDateTime().year()', patient);Important Note: As of version 2.0.0, all evaluation and analysis functions are async to support lazy loading of FHIR schemas and improved performance. Make sure to use await when calling evaluate(), compile(), analyze(), and inspect().
Parses a FHIRPath expression string into an AST (Abstract Syntax Tree) with optional parser features.
// Basic parsing (default - collects diagnostics)
const result = fhirpath.parse('Patient.name.given');
console.log(result.ast); // The parsed AST
console.log(result.diagnostics); // Array of any syntax issues
console.log(result.hasErrors); // Boolean indicating if there are errors
// Parse with error on first syntax error (fastest)
const ast = fhirpath.parseForEvaluation('Patient.name.given');
// Throws immediately on syntax errors - best for production use
// Parse with advanced features for development tools
const result = fhirpath.parse('Patient..name', {
errorRecovery: true, // Continue parsing after errors
trackRanges: true // Track source location for each AST node
});
console.log(result.isPartial); // true - indicates incomplete AST due to errors
console.log(result.ranges); // Map of AST nodes to source locationsParser Options:
throwOnError?: boolean- Throw on first error instead of collecting diagnostics (performance mode)trackRanges?: boolean- Enable source range tracking for each AST node (useful for IDEs)errorRecovery?: boolean- Enable error recovery to continue parsing after errors (useful for IDEs)maxErrors?: number- Maximum number of errors to collect before stopping
evaluate(expression: string | FHIRPathExpression, input?: any, context?: EvaluationContext): Promise<any[]>
Evaluates a FHIRPath expression against input data. Note: This function is now async.
// Evaluate with string expression
const names = await fhirpath.evaluate('name.family', patient);
// Evaluate with parsed expression
const expr = fhirpath.parse('name.family');
const names = await fhirpath.evaluate(expr, patient);
// With context variables
const result = await fhirpath.evaluate('%myVar + 5', null, {
variables: { myVar: 10 }
}); // [15]compile(expression: string | FHIRPathExpression, options?: CompileOptions): Promise<CompiledExpression>
Compiles an expression into an optimized JavaScript function for better performance. Note: Both compilation and execution are now async.
const compiled = await fhirpath.compile('name.given');
// Use compiled function multiple times (async)
const names1 = await compiled(patient1);
const names2 = await compiled(patient2);Performs static type analysis on a FHIRPath expression, with optional type checking using a FHIR model provider and error recovery for broken expressions. Note: This function is now async.
// Basic analysis
const analysis = await fhirpath.analyze('name.given');
console.log(analysis.diagnostics); // Array of any issues found
console.log(analysis.ast); // The analyzed AST with type information
// With FHIR model provider for type checking
import { FHIRModelProvider } from '@atomic-ehr/fhirpath';
const modelProvider = new FHIRModelProvider({
packages: [{ name: 'hl7.fhir.r4.core', version: '4.0.1' }]
});
await modelProvider.initialize();
const analysis = await fhirpath.analyze('Patient.birthDate.substring(0, 4)', {
modelProvider
});
// Will report type error: substring() expects String but birthDate is date
// With error recovery for IDE/tooling scenarios
const analysis = await fhirpath.analyze('Patient.name.', {
errorRecovery: true, // Won't throw on syntax errors
modelProvider
});
console.log(analysis.diagnostics); // Contains parse error: "Expected identifier after '.'"
console.log(analysis.ast); // Partial AST with error nodes
// With input type context
const analysis = await fhirpath.analyze('name.given', {
modelProvider,
inputType: {
type: 'HumanName',
namespace: 'FHIR',
singleton: false
}
});Analyze Options:
modelProvider?: ModelProvider- Provides type information for FHIR resources and data typeserrorRecovery?: boolean- Enable error tolerance for broken expressions (useful for IDEs)variables?: Record<string, unknown>- Variables available in the expression contextinputType?: TypeInfo- Type information for the input context
inspect(expression: string | FHIRPathExpression, input?: any, context?: EvaluationContext, options?: InspectOptions): Promise<InspectResult>
Evaluates an expression while capturing rich debugging information including traces, execution time, and AST. Note: This function is now async.
// Basic usage - capture trace output
const result = await fhirpath.inspect(
'name.trace("names").given.trace("given names")',
patient
);
console.log(result.result); // ['John', 'James', 'Johnny']
console.log(result.traces); // Array of trace entries
console.log(result.executionTime); // Time in milliseconds
console.log(result.ast); // Parsed AST
// Access trace information
result.traces.forEach(trace => {
console.log(`${trace.name}: ${JSON.stringify(trace.values)}`);
console.log(` at ${trace.timestamp}ms, depth: ${trace.depth}`);
});
// With options
const detailedResult = await fhirpath.inspect(
'Patient.name.where(use = "official")',
bundle,
undefined,
{
maxTraces: 100, // Limit number of traces collected
recordSteps: true // Future: enable step-by-step recording
}
);
// Error handling
const errorResult = await fhirpath.inspect('invalid.expression()');
if (errorResult.errors) {
console.log('Errors:', errorResult.errors);
}The InspectResult contains:
result: The evaluation result (same asevaluate())expression: The original expression stringast: The parsed Abstract Syntax TreeexecutionTime: Total execution time in millisecondstraces: Array of trace entries fromtrace()callserrors: Any errors encountered during evaluationwarnings: Any warnings (optional)evaluationSteps: Step-by-step evaluation details (when enabled)
Parses an expression and returns just the AST, throwing on any syntax errors. This is the most performant option for production use.
try {
const ast = fhirpath.parseForEvaluation('Patient.name.given');
// Use ast for evaluation
} catch (error) {
console.error('Parse error:', error.message);
}Validates a FHIRPath expression syntax without throwing errors.
const validation = fhirpath.validate('Patient..name');
if (!validation.valid) {
console.log('Syntax errors:', validation.diagnostics);
}The registry provides introspection capabilities for available operations.
// List all available functions
const functions = fhirpath.registry.listFunctions();
console.log(functions.map(f => f.name)); // ['where', 'select', 'first', ...]
// Check if operation exists
fhirpath.registry.hasFunction('where'); // true
fhirpath.registry.hasOperator('+'); // true
// Get operation details
const whereInfo = fhirpath.registry.getOperationInfo('where');
console.log(whereInfo.syntax.notation); // "where(expression)"
// Validate custom function names
fhirpath.registry.canRegisterFunction('myFunc'); // true
fhirpath.registry.canRegisterFunction('where'); // false (built-in)The FHIR Model Provider enables advanced type checking and validation for FHIR resources:
import { FHIRModelProvider, analyze } from '@atomic-ehr/fhirpath';
// Create and initialize the model provider
const modelProvider = new FHIRModelProvider({
packages: [{ name: 'hl7.fhir.r4.core', version: '4.0.1' }]
});
// Initialize before use (loads FHIR type definitions)
await modelProvider.initialize();
// Use with analyze for type checking (async)
const result = await analyze('Patient.active.substring(0, 1)', { modelProvider });
// Diagnostics: "Type mismatch: function 'substring' expects input type String but got Boolean"
// Type-aware property navigation
const result2 = await analyze('Patient.birthDate.year()', { modelProvider });
// Works correctly - birthDate is recognized as a date type
// Detect invalid property access
const result3 = await analyze('Patient.invalidProperty', { modelProvider });
// Diagnostics: "Unknown property 'invalidProperty' on type FHIR.Patient"
// Works with complex paths
const result4 = await analyze(
'Bundle.entry.resource.where(resourceType = "Patient").name.given',
{ modelProvider }
);
// Validates entire path with proper type informationThe model provider supports:
- Property validation and type checking
- Polymorphic type resolution
- Choice type handling (e.g.,
value[x]) - Extension navigation
- Full FHIR R4 type system
For advanced configurations, use the builder pattern:
import { FHIRPath } from '@atomic-ehr/fhirpath';
const fp = FHIRPath.builder()
// Add custom functions
.withCustomFunction('double', async (context, input) => {
return input.map(x => x * 2);
})
// Set default variables
.withVariable('defaultStatus', 'active')
// Add model provider for type information
.withModelProvider({
getType: async (typeName) => { /* ... */ },
getElementType: async (parentType, propertyName) => { /* ... */ },
ofType: (type, typeName) => { /* ... */ },
getElementNames: (parentType) => { /* ... */ },
getChildrenType: async (parentType) => { /* ... */ }
})
.build();
// Use the configured instance (async)
const result = await fp.evaluate('value.double()', { value: [5] }); // [10]
const status = await fp.evaluate('%defaultStatus'); // ['active']Custom functions extend FHIRPath with domain-specific operations:
const fp = FHIRPath.builder()
.withCustomFunction('age', async (context, input) => {
// Calculate age from birthDate
return input.map(birthDate => {
const today = new Date();
const birth = new Date(birthDate);
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--;
}
return age;
});
})
.withCustomFunction('fullName', async (context, input) => {
return input.map(name => {
if (name && typeof name === 'object') {
const given = Array.isArray(name.given) ? name.given.join(' ') : '';
const family = name.family || '';
return `${given} ${family}`.trim();
}
return '';
});
})
.build();
// Use custom functions (async)
const age = await fp.evaluate('birthDate.age()', patient);
const fullNames = await fp.evaluate('name.fullName()', patient);All API functions throw FHIRPathError for invalid expressions or runtime errors:
import { FHIRPathError, ErrorCode } from '@atomic-ehr/fhirpath';
try {
fhirpath.parse('invalid..expression');
} catch (error) {
if (error instanceof FHIRPathError) {
console.error(`Error: ${error.message}`);
console.error(`Code: ${error.code}`);
console.error(`Location: Line ${error.location?.line}, Column ${error.location?.column}`);
}
}The library is fully typed with TypeScript:
import type {
FHIRPathExpression,
CompiledExpression,
EvaluationContext,
ModelProvider,
CustomFunction,
OperationInfo,
AnalysisResult,
InspectResult,
InspectOptions,
// Parser types
ParserOptions,
ParseResult,
ParseDiagnostic,
DiagnosticSeverity,
TextRange,
ASTNode,
// Analyzer types
AnalyzeOptions,
Diagnostic,
TypeInfo,
// FHIR Model Provider
FHIRModelProvider,
FHIRModelProviderConfig
} from '@atomic-ehr/fhirpath';The analyzer supports error recovery mode for IDE and tooling scenarios, allowing analysis of incomplete or syntactically incorrect expressions:
// Normal mode - throws on syntax errors
try {
const result = await fhirpath.analyze('Patient.name.');
} catch (error) {
console.error('Syntax error:', error.message);
}
// Error recovery mode - continues analysis despite errors
const result = await fhirpath.analyze('Patient.name.', {
errorRecovery: true
});
// Check diagnostics instead of catching errors
console.log(result.diagnostics);
// [{
// message: "Expected identifier after '.', got: EOF",
// severity: DiagnosticSeverity.Error,
// range: { start: { line: 0, character: 13 }, end: { line: 0, character: 13 } }
// }]
// The AST contains error nodes but analysis continues
console.log(result.ast); // Partial AST with error nodes
// Complex broken expression - multiple errors reported
const result2 = await fhirpath.analyze('Patient.name.where(use = ).given.', {
errorRecovery: true,
modelProvider
});
console.log(result2.diagnostics.length); // 2 errors
// Type information is preserved for valid parts
const result3 = await fhirpath.analyze('5 + 3 * ', {
errorRecovery: true
});
// Even though expression is incomplete, literals 5 and 3 have type infoThis is particularly useful for:
- Language servers: Provide diagnostics while users type
- IDE plugins: Show errors inline without breaking analysis
- Code completion: Analyze partial expressions for context
- Linting tools: Report all issues in a single pass
const bundle = {
resourceType: 'Bundle',
entry: [
{ resource: { resourceType: 'Patient', id: '1', active: true } },
{ resource: { resourceType: 'Patient', id: '2', active: false } },
{ resource: { resourceType: 'Observation', status: 'final' } }
]
};
// Get all patients
const patients = await fhirpath.evaluate(
'entry.resource.where(resourceType = \'Patient\')',
bundle
);
// Get active patients
const activePatients = await fhirpath.evaluate(
'entry.resource.where(resourceType = \'Patient\' and active = true)',
bundle
);
// Count resources by type
const patientCount = await fhirpath.evaluate(
'entry.resource.where(resourceType = \'Patient\').count()',
bundle
); // [2]const observations = [
{ code: { coding: [{ system: 'loinc', code: '1234' }] }, value: 140 },
{ code: { coding: [{ system: 'loinc', code: '1234' }] }, value: 120 },
{ code: { coding: [{ system: 'loinc', code: '5678' }] }, value: 98.6 }
];
// Find high blood pressure readings
const highBP = await fhirpath.evaluate(
'where(code.coding.exists(system = \'loinc\' and code = \'1234\') and value > 130)',
observations
);const patient = {
birthDate: '1990-05-15'
};
// Check if patient is adult (>= 18 years)
const isAdult = await fhirpath.evaluate(
'today() - birthDate.toDateTime() >= 18 years',
patient
);Use the inspect() function to debug complex FHIRPath expressions:
const bundle = {
entry: [
{ resource: { resourceType: 'Patient', name: [{ given: ['John'] }] } },
{ resource: { resourceType: 'Patient', name: [{ given: ['Jane'] }] } }
]
};
// Debug a complex expression with traces
const result = await fhirpath.inspect(
`entry.resource
.trace('all resources')
.where(resourceType = 'Patient')
.trace('patients only')
.name.given
.trace('all given names')`,
bundle
);
// Analyze the execution
console.log('Result:', result.result);
console.log('Execution time:', result.executionTime + 'ms');
console.log('\nTrace output:');
result.traces.forEach(trace => {
console.log(`- ${trace.name}: ${trace.values.length} items`);
});
// Output:
// Result: ['John', 'Jane']
// Execution time: 0.523ms
//
// Trace output:
// - all resources: 2 items
// - patients only: 2 items
// - all given names: 2 items-
Use Fast Parsing for Production: When parsing expressions in production, use
parseForEvaluation()or enablethrowOnError:// Fastest - throws on first error const ast = fhirpath.parseForEvaluation('name.given'); // Or use throwOnError option const result = fhirpath.parse('name.given', { throwOnError: true });
-
Parse Once, Evaluate Many: Parse expressions once and reuse the parsed AST:
const expr = fhirpath.parse('name.given'); for (const patient of patients) { const names = await fhirpath.evaluate(expr, patient); }
-
Use Compiled Functions: For expressions evaluated frequently, use compilation:
const getName = await fhirpath.compile('name.given'); const results = await Promise.all(patients.map(p => getName(p)));
-
Disable Unnecessary Parser Features: Only enable parser features you need:
// For development tools (IDEs, linters) const result = fhirpath.parse(expr, { errorRecovery: true, // Only if you need partial ASTs trackRanges: true // Only if you need source mapping }); // For production (fastest) const ast = fhirpath.parseForEvaluation(expr);
-
Builder Instance: Create a configured instance once and reuse:
const fp = FHIRPath.builder() .withCustomFunction('myFunc', /* ... */) .build(); // Use fp instance throughout your application
See CONTRIBUTING.md for development setup and guidelines.
MIT