Type-safe HL7v2 parsing and generation for TypeScript.
At the core is a minimal, JSON-friendly data structure that captures HL7v2 semantics without wire format details:
type FieldValue = string | FieldValue[] | { [key: number]: FieldValue };
interface HL7v2Segment {
segment: string; // "MSH", "PID", etc.
fields: Record<number, FieldValue>; // 1-indexed field values
}
type HL7v2Message = HL7v2Segment[];This representation:
- Preserves position information (field 3, component 1, etc.)
- Handles repeating fields as arrays
- Handles components/subcomponents as nested objects
- Is serializable to JSON
- Is independent of wire format delimiters
Generated interfaces use positional field names with $N_name pattern:
interface PID {
$1_setIdPid?: string;
$3_identifier: CX[]; // Required (minOccurs=1)
$5_name: XPN[]; // Required
$7_birthDate?: string;
$8_gender?: string;
}
interface XPN {
$1_family?: FN;
$2_given?: string;
$3_additionalGiven?: string;
}Why $N_name:
- Position preserved -
$3_identifiertells you it's PID-3 - Readable - semantic name follows the number
- Compact - shorter than alternatives like
field_3_identifier - Neutral - same property for read and write (no
get_/set_confusion)
Building and parsing use symmetric function pairs:
Wire Format ←→ Internal ←→ Typed Objects
parseMessage() fromPID() ← Reading
formatMessage() toADT_A01() ← Writing
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Wire Format │────▶│ Internal │────▶│ Wire Format │
│ (pipe-delim) │ │ Representation │ │ (pipe-delim) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
parseMessage() HL7v2Message formatMessage()
│
┌────────────┴────────────┐
│ │
fromPID() toADT_A01()
fromMSH() ADT_A01Builder
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Typed Objects │ │ Typed Objects │
│ PID, MSH... │ │ ADT_A01_Input│
└───────────────┘ └───────────────┘
bun add @atomic-ehr/hl7v2To use this library in your own project, you need to:
- Install the package
- Run the code generator to create typed interfaces for your message types
- Import the generated code and runtime functions
bun add @atomic-ehr/hl7v2Run the generator specifying output directory and message types you need:
bunx @atomic-ehr/hl7v2/src/hl7v2/codegen.ts ./src/hl7v2-generated ADT_A01 ORU_R01This creates four files in ./src/hl7v2-generated/:
types.ts- Core typestables.ts- HL7 table constantsfields.ts- Segment and datatype interfacesmessages.ts- Message builders and converters
Building messages:
import { formatMessage } from "@atomic-ehr/hl7v2";
import { toADT_A01, type ADT_A01_Input } from "./src/hl7v2-generated/messages";
const input: ADT_A01_Input = {
type: "ADT_A01",
MSH: {
$3_sendingApplication: { $1_namespace: "MY_APP" },
$7_messageDateTime: "20251210120000",
$9_messageType: { $1_code: "ADT", $2_event: "A01" },
$10_messageControlId: "MSG001",
$11_processingId: { $1_processingId: "P" },
$12_version: { $1_version: "2.5.1" }
},
PID: {
$3_identifier: [{ $1_value: "12345" }],
$5_name: [{ $1_family: { $1_family: "Smith" }, $2_given: "John" }]
},
PV1: { $2_class: "I" }
};
const message = toADT_A01(input);
const wireFormat = formatMessage(message);Parsing messages:
import { parseMessage } from "@atomic-ehr/hl7v2";
import { fromPID, fromMSH } from "./src/hl7v2-generated/fields";
const wire = `MSH|^~\\&|SENDER|||20231201||ADT^A01|MSG001|P|2.5.1
PID|1||12345^^^HOSP||Smith^John||19800515|M`;
const message = parseMessage(wire);
const pid = fromPID(message.find(s => s.segment === "PID")!);
console.log(pid.$3_identifier?.[0]?.$1_value); // "12345"
console.log(pid.$5_name?.[0]?.$2_given); // "John"Common message types you can generate:
| Type | Description |
|---|---|
ADT_A01 |
Patient admission |
ADT_A04 |
Patient registration |
ADT_A08 |
Patient information update |
ORU_R01 |
Lab results |
ORM_O01 |
General order |
BAR_P01 |
Billing account |
SIU_S12 |
Scheduling notification |
MDM_T02 |
Document notification |
Generate multiple types at once:
bunx @atomic-ehr/hl7v2/src/hl7v2/codegen.ts ./generated ADT_A01 ADT_A04 ADT_A08 ORU_R01The examples below assume you're working within this repository. For external projects, see Using in External Projects.
Option 1: Input Object (simplest)
import { toADT_A01, type ADT_A01_Input } from "./example/messages";
import { formatMessage } from "./src/hl7v2/format";
const input: ADT_A01_Input = {
type: "ADT_A01",
MSH: {
$3_sendingApplication: { $1_namespace: "HOSPITAL" },
$7_messageDateTime: "20251210120000",
$9_messageType: { $1_code: "ADT", $2_event: "A01" },
$10_messageControlId: "MSG001",
$11_processingId: { $1_processingId: "P" },
$12_version: { $1_version: "2.5.1" }
},
EVN: { $2_recordedDateTime: "20251210120000" },
PID: {
$3_identifier: [{ $1_value: "12345", $4_system: { $1_namespace: "MRN" } }],
$5_name: [{ $1_family: { $1_family: "Smith" }, $2_given: "John" }]
},
PV1: { $2_class: "I" },
DG1: [{ $1_setIdDg1: "1", $3_diagnosisCodeDg1: { $1_code: "J18.9" }, $6_diagnosisType: "A" }],
PROCEDURE: [{ PR1: { $1_setIdPr1: "1", $3_procedureCode: { $1_code: "99213" }, $5_procedureDateTime: "20251210" } }]
};
const message = toADT_A01(input);
const wire = formatMessage(message);Option 2: Fluent Builder (for incremental construction)
import { ADT_A01Builder } from "./example/messages";
const message = new ADT_A01Builder()
.msh({ $7_messageDateTime: "20251210120000", /* ... */ })
.evn({ $2_recordedDateTime: "20251210120000" })
.pid({ $3_identifier: [{ $1_value: "12345" }], $5_name: [{ $1_family: { $1_family: "Smith" } }] })
.pv1({ $2_class: "I" })
.addDG1({ $1_setIdDg1: "1", $3_diagnosisCodeDg1: { $1_code: "J18.9" }, $6_diagnosisType: "A" })
.build();import { parseMessage } from "./src/hl7v2/parse";
import { fromPID, fromMSH } from "./example/fields";
const wire = `MSH|^~\\&|HOSPITAL|FAC|||20231201||ADT^A01|MSG001|P|2.5.1
PID|1||12345^^^HOSP^MR||Smith^John||19800515|M`;
// Parse to internal representation
const message = parseMessage(wire);
// Convert to typed objects
const pid = fromPID(message.find(s => s.segment === "PID")!);
console.log(pid.$3_identifier?.[0]?.$1_value); // "12345"
console.log(pid.$5_name?.[0]?.$1_family?.$1_family); // "Smith"
console.log(pid.$5_name?.[0]?.$2_given); // "John"
console.log(pid.$8_gender); // "M"Generate type-safe code from HL7v2 schema:
bun src/hl7v2/codegen.ts ./output ADT_A01 BAR_P01This generates self-contained files:
| File | Contents |
|---|---|
types.ts |
Core types: FieldValue, HL7v2Segment, HL7v2Message |
tables.ts |
Constants: AdministrativeSex, PatientClass, etc. |
fields.ts |
DataTypes (XPN, CX), Segments (PID, MSH), Getters (fromPID) |
messages.ts |
Input interfaces (ADT_A01_Input), Converters (toADT_A01), Builders |
Generated code is standalone - no runtime dependency on src/hl7v2/.
src/hl7v2/
├── types.ts # Core types
├── parse.ts # Wire → Internal
├── format.ts # Internal → Wire
└── codegen.ts # Schema → TypeScript
schema/
├── messages/ # Message structures (ADT_A01.json, etc.)
├── segments/ # Segment definitions (PID.json, etc.)
├── fields/ # Field metadata
└── dataTypes/ # Complex types (XPN, CX, etc.)
example/
├── adt-a01-example.ts # Builder pattern
├── input-example.ts # Input object pattern
├── parse-example.ts # Parsing with getters
└── *.ts # Generated code
bun example/adt-a01-example.ts # Build with builder
bun example/input-example.ts # Build with input object
bun example/parse-example.ts # Parse and extractInteractive web tool for parsing and highlighting HL7v2 messages:
bun run debugThen open http://localhost:3333
Features:
- Syntax highlighting with hover tooltips showing field metadata
- Schema-based parsing to named fields (e.g.,
$3_identifier,$5_name) - Sample message for quick testing
bun testCreated by Nikolai Ryzhikov. Sponsored by Health Samurai.
HL7v2 schema from redox-hl7-v2.
MIT