gedcom-ts
v2026.6.0
Published
TypeScript GEDCOM toolkit for browser apps: import .ged/.zip, edit typed genealogy data, and export GEDCOM or GEDZIP.
Maintainers
Readme
gedcom-ts
gedcom-ts is a browser-oriented TypeScript library to:
- import genealogy data from GEDCOM (
.ged) or ZIP (.zip/.gdz) - work with a typed JSON model (persons, acts, dates, places, notes, media, name variants, attributes)
- edit the model in-place through a fluent, chainable API (
editPerson,editAct, …) - export data back to GEDCOM (
.ged) or GEDZIP (.zip) - attach audio and other media via GEDCOM 7
OBJErecords (MEDI AUDIO, multi-FILE,TRAN) - model oral history interviews as
EVENacts withOBJE+ transcription notes - geocode event places (OpenStreetMap / Nominatim) and group similar city names
- generate a genealogy booklet as PDF (
gedcom-ts/booklet, 11 locales including RTL Arabic/Hebrew)
Live demo
A graphical demo showcasing the public API (import a .ged / .zip, browse the typed model, export back) is available at https://gedcomts.com
Contents
- Installation
- Quick start
- Geocoding places
- Genealogy booklet (PDF)
- Audio and oral history (GEDCOM 7)
- API reference
- Error handling
Package
- NPM: gedcom-ts
- Current release
2026.6.0— audio / OBJE / oral history. Version format CalVerAAAA.M.micro(e.g.2026.6.0= June 2026). See CHANGELOG.md. - Entry points:
gedcom-ts— import, model, edit layer, export, geocodinggedcom-ts/booklet— PDF livret (pdf-liben peerDependency, chargé depuisnode_modules)
Installation
npm install gedcom-tsBoth entry points come from the same package. For the PDF booklet, pdf-lib is a peer dependency — install it in your app if you use gedcom-ts/booklet:
npm install pdf-libFor zh / ar / he PDF output, your bundler should also be able to resolve @pdf-lib/fontkit (included in gedcom-ts dependencies; the host app typically preloads it — see Bundle layout below).
Runtime Requirements
- modern browser runtime (
File,Blob,XMLHttpRequest,URL.createObjectURL) - for pure Node.js usage, DOM polyfills are required (the library targets browsers)
Quick start
import { importGedFile, ExportGedzipFile } from "gedcom-ts";
async function roundTrip(file: File) {
const readGed = await importGedFile(file);
const persons = readGed.persons;
if (persons.length > 0) {
persons[0].lastname = persons[0].lastname.toUpperCase();
}
await new ExportGedzipFile("updated-tree", persons).download();
}Typical workflow:
- import a file with
importGedFile(or start fromcreateEmptyReadGed()) - read / mutate the typed
Person,Act,Place,Note,MultimediaFileobjects (directly or througheditPerson/editAct/editPlace/ …) - export as
.ged(ExportGedcomFile) or.zip(ExportGedzipFile)
Geocoding places
Use this section when your app needs GPS coordinates on event places, a list of cities still without coordinates, map markers, or a data-quality view for inconsistent place names.
You work with a ReadGed (after import) and a flat Act[] built from every person’s events (see Prepare act list).
How city names are matched
The library groups variants of the same place with citiesAreSimilar, not exact string equality.
| Label A | Label B | Same place? |
| ----------- | ------------------------------------ | --------------------------- |
| Pleurtuit | Pleurtuit, Ille-et-Vilaine, France | Yes |
| Paris | Paris, TX, USA | Depends on similarity rules |
Use the functions in the tables below for grouping, geocoding, and maps. Do not compare raw strings yourself, and do not use normalizeCityKey for grouping (spelling normalization only).
Prepare act list
import type { ReadGed, Act } from "gedcom-ts";
function collectAllActs(ged: ReadGed): Act[] {
const acts: Act[] = [];
for (const person of ged.persons) {
for (const act of person.acts.list) acts.push(act);
}
return acts;
}Geocode one city (typical flow)
When the user picks a city and a Nominatim result, update every similar act that still has no GPS — not only acts with the exact same label.
| Step | Function |
| ---------------------------------------------- | ----------------------------------------------------------- |
| 1. Context from the tree (countries, centroid) | inferGeocodeContext(ged) |
| 2. Search OpenStreetMap | searchPlacesWithContext(cityName, context) |
| 3. Acts to update | actsNeedingGeocodeForCity(allActs, cityName) |
| 4. Write coordinates | applyGeocodeCandidateToActs(targets, candidate, cityName) |
import {
inferGeocodeContext,
searchPlacesWithContext,
actsNeedingGeocodeForCity,
applyGeocodeCandidateToActs,
} from "gedcom-ts";
import type { ReadGed, Act } from "gedcom-ts";
async function geocodeCity(ged: ReadGed, allActs: Act[], cityName: string) {
const context = inferGeocodeContext(ged);
const candidates = await searchPlacesWithContext(cityName, context);
const chosen = candidates[0];
if (!chosen) return;
const targets = actsNeedingGeocodeForCity(allActs, cityName);
applyGeocodeCandidateToActs(targets, chosen, cityName);
}Tip: Do not pass only acts from a harmonization cluster to
applyGeocodeCandidateToActs. Longer labels (e.g.Pleurtuit, Ille-et-Vilaine, France) would stay without GPS. Always useactsNeedingGeocodeForCity.
List cities missing coordinates
One row per city; withoutCoordCount is how many acts still need GPS.
import { groupActsBySimilarCity } from "gedcom-ts";
const groups = groupActsBySimilarCity(allActs, { onlyWithoutCoordinates: true });
for (const group of groups) {
console.log(group.cityLabel, group.withoutCoordCount);
// group.acts — acts in this group without GPS
}Largest groups first (group.acts.length).
Map: one marker per city
import { clusterKeyForCity } from "gedcom-ts";
const markerKey = clusterKeyForCity(act.place?.city ?? "");
// merge markers that share the same markerKeyHarmonization (data quality, optional)
Use when spellings, GPS positions, or “some acts with / without GPS” disagree for the same similar city.
import { findHarmonizationClusters } from "gedcom-ts";
for (const cluster of findHarmonizationClusters(ged)) {
console.log(cluster.labels, cluster.coordVariants, cluster.actsWithoutCoord);
}After a full geocode via actsNeedingGeocodeForCity, you should not get a cluster that only reports missing GPS for that city.
Geocoding API cheat sheet
| Goal | Call |
| ------------------------- | ---------------------------------------------------------------- |
| Search | searchPlacesWithContext(city, inferGeocodeContext(ged)) |
| Acts to update on confirm | actsNeedingGeocodeForCity(acts, city) |
| Apply lat/lng | applyGeocodeCandidateToActs(targets, candidate, city) |
| Cities without GPS | groupActsBySimilarCity(acts, { onlyWithoutCoordinates: true }) |
| Map marker id | clusterKeyForCity(city) |
| Inconsistencies | findHarmonizationClusters(ged) |
Network: GET https://nominatim.openstreetmap.org/search?q=…&format=jsonv2 with a User-Agent (gedcom-ts/<version> (genealogy library)). For offline or mocked search, pass fetchFn in searchPlaces / searchPlacesWithContext options.
The legacy callback getCityCoordinates is deprecated — use the flow above.
Genealogy booklet (PDF)
The gedcom-ts/booklet subpath builds a printable family booklet: cover page, table of contents, chapters by generation (Sosa), family sheets, and narrative text from GEDCOM acts (birth, marriage, death, …). English is the default locale (DEFAULT_BOOKLET_LOCALE). Full copy is available for en, fr, de, nl, es, zh, it, pt, pl, ar, and he (aligned with gedcomts.com UI languages). Arabic and Hebrew use RTL layout in the PDF (isBookletLocaleRtl, bookletTextDirection).
Bundle layout
Code is split so the host app only downloads what it uses. Approximate sizes after minify (obfuscation adds ~25–30 KiB on gedcom-ts/booklet).
| Artifact | Size (typ.) | When loaded |
| --- | ---: | --- |
| gedcom-ts (dist/index.mjs) | ~180 KiB | GEDCOM import / edit / export |
| gedcom-ts/booklet (dist/booklet.mjs) | ~100 KiB | livret (collect, estimate, PDF) |
| locale-chunks/booklet-locale-* | 14–26 KiB | one UI language |
| feature-chunks/booklet-logo-draw | ~11 KiB | cover logo (coverLogo !== false) |
| feature-chunks/booklet-timeline-raster | ~3 KiB | timelineStyle: "canvas" |
| font-chunks/booklet-pdf-font-bytes-ar | ~43 KiB | Arabic PDF |
| font-chunks/booklet-pdf-font-bytes-he | ~13 KiB | Hebrew PDF |
| font-chunks/booklet-pdf-font-bytes-zh | ~190 KiB | Chinese PDF (base subset) |
| font-chunks/booklet-pdf-font-bytes-zh-ext | ~1.4 MiB | Chinese PDF when GEDCOM names need extra Han |
| pdf-lib (peer, not in gedcom-ts) | ~500 KiB+ | any PDF generation |
Not bundled in gedcom-ts: fflate (main entry, external), pdf-lib (booklet peer). bidi-js and naqqash (Arabic shaping / RTL reordering) ship inside gedcom-ts/booklet today.
Call await ensureBookletLocale(locale) before sync APIs. generateGenealogyBookletPdf preloads locale, logo, and timeline when needed.
Lazy chunk subpaths (for bundler preload or explicit import() — normal apps should use ensureBookletLocale() instead):
| Subpath | Role |
| --- | --- |
| gedcom-ts/booklet/locale-chunks/booklet-locale-{en,fr,de,nl,es,zh,it,pt,pl,ar,he} | Messages, narratives, date/place helpers per locale |
| gedcom-ts/booklet/font-chunks/booklet-pdf-font-bytes-{ar,he,zh,zh-ext} | Subset Noto font bytes |
| gedcom-ts/booklet/feature-chunks/booklet-logo-draw | Cover logo SVG paths |
| gedcom-ts/booklet/feature-chunks/booklet-timeline-raster | Canvas timeline PNG rasterizer |
Each subpath is listed in package.json exports with matching types (.d.ts under dist/booklet/…) and runtime (.mjs / .cjs under dist/…). TypeScript resolves them via exports.types (moduleResolution: bundler / node16). Adding a locale updates scripts/booklet-chunk-manifest.mjs; npm run build:js regenerates chunk files and syncs exports.
Vite / Angular integration
- Lazy-load the booklet entry —
import('gedcom-ts/booklet')only when the user opens export / download (not on every tree page). - Preload the UI locale —
import('gedcom-ts/booklet/locale-chunks/booklet-locale-fr')(or your loader) before sync APIs; thenensureBookletLocale('fr'). - Unicode PDF (
zh/ar/he) — the host app should import font chunks explicitly and callregisterBookletPdfFontBytes+registerPdfFontkitbeforegenerateGenealogyBookletPdf(see gedcomts.combooklet-pdf-deps.loader.tsfor a reference pattern). - Dev server — exclude the main booklet bundle from Vite pre-bundling so lazy subpaths resolve via
node_modules:
// vite.config.ts / angular.json (esbuild optimizeDeps)
optimizeDeps: {
exclude: ["gedcom-ts/booklet"],
},Published dist/booklet.mjs keeps literal dynamic import('gedcom-ts/booklet/…') strings (rewrite before obfuscation, paths reserved from string encoding) so Vite can statically analyze chunk loads.
Workflow
- Import with
importGedFile(gedcom-ts). await ensureBookletLocale(locale)— load messages/narratives for the chosen language.- Collect persons with
collectBookletPersons(gedcom-ts/booklet). - Optionally preview size with
estimateBookletSize+groupBookletIntoChapters. - Build bytes with
generateGenealogyBookletPdf, thendownloadBookletPdf(browser).
import { importGedFile } from "gedcom-ts";
import {
collectBookletPersons,
ensureBookletLocale,
groupBookletIntoChapters,
estimateBookletSize,
generateGenealogyBookletPdf,
downloadBookletPdf,
personDisplayName,
} from "gedcom-ts/booklet";
async function exportBooklet(file: File) {
const ged = await importGedFile(file);
const root = ged.persons[0] ?? null;
const locale = "en" as const; // BookletLocale — align with your UI language (en, fr, de, nl, es, zh, it, pt, pl, ar, he)
await ensureBookletLocale(locale);
const entries = collectBookletPersons({
ged,
scope: "from-reference",
referencePerson: root,
maxGeneration: 6,
locale,
});
const chapters = groupBookletIntoChapters(entries, locale);
const families = chapters.reduce((n, ch) => n + ch.families.length, 0);
const size = estimateBookletSize(chapters, entries.length, families, "summary", "canvas", locale);
console.log(size.label);
const pdf = await generateGenealogyBookletPdf(
entries,
{
title: "Family booklet",
scopeLabel: "Ancestors and descendants",
personCount: entries.length,
referenceName: root ? personDisplayName(root) : undefined,
},
{
detailLevel: "summary",
timelineStyle: "canvas",
coverLogo: true,
locale,
},
);
downloadBookletPdf(pdf, "family-booklet.pdf");
}Use scope: "all" and referencePerson: null to include every individual in the file.
collectBookletPersons options
| Option | Role |
| --- | --- |
| ged | ReadGed after import |
| scope | "all" or "from-reference" (Sosa from referencePerson) |
| referencePerson | Root person for "from-reference"; null if scope is "all" |
| maxGeneration | Max Sosa generation (e.g. 6); ignored when scope === "all" |
| locale | BookletLocale (default "en") — en, fr, de, nl, es, zh, it, pt, pl, ar, he |
Helpers: personDisplayName, buildBookletPersonEntry, sortBookletEntries, bookletSexFromPerson, DEFAULT_BOOKLET_LOCALE, BOOKLET_LOCALES, isBookletLocaleRtl.
generateGenealogyBookletPdf options
| Option | Values | Effect |
| --- | --- | --- |
| detailLevel | "summary" | "detailed" | Short prose per person vs longer biographies |
| timelineStyle | "off" | "canvas" | Generation timeline pages per chapter |
| coverLogo | true (default), false, or DrawGedcomTsLogoOptions | gedcom-ts vector logo on the cover |
| locale | BookletLocale (default "en") | Narrative, chapter titles, PDF chrome, timeline legend |
| unicodeFont | Uint8Array (optional) | Replace bundled Noto font for zh / ar / he |
BookletPdfMeta: title, scopeLabel, personCount, optional referenceName (usually translated in the host app).
Cover logo
The cover draws the gedcom-ts logo from lazy-loaded SVG paths (ensureBookletLogo, getGedcomTsLogoPaths, getGedcomTsLogoViewbox). Reuse on custom PDF pages:
import { PDFDocument } from "pdf-lib";
import {
drawGedcomTsLogoOnPage,
defaultCoverLogoOptions,
ensureBookletLogo,
} from "gedcom-ts/booklet";
const doc = await PDFDocument.create();
const page = doc.addPage();
await ensureBookletLogo();
await drawGedcomTsLogoOnPage(page, defaultCoverLogoOptions());Booklet API cheat sheet
| Goal | Export |
| --- | --- |
| Persons for the booklet | collectBookletPersons |
| Chapters / families | groupBookletIntoChapters, partnerNamesLabel |
| Localized narratives (custom UI) | buildPersonSummaryNarrative, buildPersonDetailedNarratives, buildFamilyNarrative, buildChapterIntroNarrative |
| Page estimate | estimateBookletSize, bookletSizeAdvice |
| Timeline data / PNG | buildGenerationTimeline, rasterizeGenerationTimelinePng |
| PDF output | generateGenealogyBookletPdf, downloadBookletPdf, toPdfText, preparePdfText |
| Locale helpers | resolveBookletLocale, bookletLocaleToBcp47, bookletTextDirection, BOOKLET_RTL_LOCALES, ensureBookletLocale |
| Font preload (host) | registerBookletPdfFontBytes, registerPdfFontkit, localeNeedsUnicodePdfFont |
| Logo | ensureBookletLogo, drawGedcomTsLogoOnPage, getGedcomTsLogoPaths, defaultCoverLogoOptions |
Audio and oral history (GEDCOM 7)
Since 2026.6.0, gedcom-ts models audio according to FamilySearch GEDCOM 7: external files referenced by OBJE records (FILE + FORM + optional MEDI AUDIO + TRAN for alternate formats or WebVTT). Text transcriptions belong in NOTE under the event, not inside OBJE.
Oral history pattern: 1 EVEN + 2 TYPE Interview (or Oral history) + 2 OBJE @O…@ + 2 NOTE (transcription).
import {
createAudioInterviewAct,
editAct,
ExportGedzipFile,
type Person,
type ReadGed,
} from "gedcom-ts";
async function addInterview(readGed: ReadGed, person: Person, mp3: File) {
const objeMap = new Map(readGed.objeRecordsById);
const { act, objeRecord } = createAudioInterviewAct(
{
description: "Entretien avec grand-mère",
participantIndis: [person.INDI],
ownerIndi: person.INDI,
audioFile: mp3,
transcription: "Bonjour, je m'appelle Marie…",
vttUri: "media/transcript.vtt", // optional FILE.TRAN on OBJE
},
readGed.persons,
objeMap,
);
person.acts.add(act);
if (objeRecord) {
objeMap.set(objeRecord.id, objeRecord);
}
await new ExportGedzipFile("my-tree", readGed.persons, {
extraTopLevelRecords: readGed.preservedTopLevelRecords,
objeRecordsById: objeMap,
}).download();
}
// enrich an existing act:
editAct(act)
.asAudioInterview(readGed.persons, objeMap)
.setTranscription("Suite de l'entretien…")
.attachAudio({ audioUri: "https://example.org/rec.mp3", ownerIndi: person.INDI });| Goal | API |
| --- | --- |
| Create interview act | createAudioInterviewAct |
| Attach audio to act | attachAudioInterviewToAct, editAct(act).asAudioInterview() |
| Detect imported interviews | isAudioInterviewAct, isAudioInterviewEvenType |
| Read transcription / media | getAudioInterviewTranscription, primaryAudioInterviewUri |
| OBJE round-trip | readGed.objeRecordsById → objeRecordsById in export options |
| Low-level OBJE parse/emit | parseStandaloneObjeBlock, formatObjeRecordBlock, guessMediFromForm |
| EVEN type constants | GEDCOM_7_EVEN_TYPE_AUDIO_INTERVIEW, GEDCOM_7_EVEN_TYPE_ORAL_HISTORY |
API reference
Short description and a minimal snippet for each export of gedcom-ts. For the PDF booklet, see Genealogy booklet (PDF) (gedcom-ts/booklet).
Importing a file
importGedFile(file: File): Promise<ReadGed>
Detects the format from the file MIME / extension and dispatches to the right reader.
- accepts a single
.gedfile - accepts a
.zip/.gdzarchive containing one.ged+ optional media files - throws
Error(IMPORT_ERR_ZIP_GED_UNREADABLE)when the embedded.gedcannot be decoded (typical cause: password-protected ZIP)
import { importGedFile } from "gedcom-ts";
const readGed = await importGedFile(fileInput.files![0]);
console.log(readGed.persons.length, readGed.datasetVersion);createEmptyReadGed(options?): ReadGed
Creates an empty graph with the same runtime shape as a successful import (empty persons, initialized mapPersons / partnersMap / childsMap / placesMap / mapFiles). Use it to start a brand-new tree without parsing a file. datasetVersion is set to "7.0".
import { createEmptyReadGed } from "gedcom-ts";
const readGed = createEmptyReadGed();IMPORT_ERR_ZIP_GED_UNREADABLE: string
Sentinel error message thrown by importGedFile when a .ged inside a ZIP cannot be decoded (typically because the ZIP is password-protected). Compare with error.message === IMPORT_ERR_ZIP_GED_UNREADABLE to display a tailored message.
import { importGedFile, IMPORT_ERR_ZIP_GED_UNREADABLE } from "gedcom-ts";
try {
await importGedFile(file);
} catch (error) {
if (error instanceof Error && error.message === IMPORT_ERR_ZIP_GED_UNREADABLE) {
alert("Please unzip the archive manually and import the .ged file.");
}
}ReadGed
Result of an import. Standalone 0 @O…@ OBJE blocks with at least one FILE are parsed into objeRecordsById (multi-file, MEDI, TRAN). Other top-level records (REPO, SOUR, SUBM, …) stay on preservedTopLevelRecords for round-trip export.
Notable members:
| Member | Description |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| persons: Person[] | Imported individuals. |
| mapPersons: Map<number, Person> | INDI (integer) → Person. |
| partnersMap: Map<number, Person[]> | Family id → spouses. |
| childsMap: Map<number, Person[]> | Family id → children. |
| placesMap: Map<string, Place> | City name → Place (first occurrence wins). |
| mapFiles: Map<string, File> | Relative path → media File (for ZIP imports). |
| datasetVersion: GedcomDatasetVersion | "7.0", "5.5" or "unknown". |
| objeRecordsById: Map<number, ObjeRecord> | Parsed OBJE records (audio, images, …) keyed by @O{n}@. |
| preservedTopLevelRecords: string[] | Raw blocks for partial round-trip. |
| resolveIndividualPointer(raw) | Resolves @I12@, I12, @Homer_Simpson@, Homer_Simpson to a Person. |
| getChildrenForParent(parent) | All children attached to a parent (via FAMS). |
| getChildrenOfFamily(familyId) | Children of a single family. |
| groupPartners() | Rebuilds partnersMap / childsMap after editing links. |
| generateUniqueIndi() | Next free INDI number for new persons. |
| rehydratePlacesFromActs() | Rebuilds placesMap from act places (e.g. after manual graph edits). Use editReadGed(readGed).addPerson(...) to register persons so maps stay coherent. |
import { ReadGed } from "gedcom-ts";
function describe(readGed: ReadGed) {
return {
version: readGed.datasetVersion,
persons: readGed.persons.length,
places: readGed.placesMap.size,
};
}Exporting
ExportGedcomFile
Writes a GEDCOM 7 (.ged) file. Three call signatures are supported:
new ExportGedcomFile(persons);
new ExportGedcomFile(title, persons);
new ExportGedcomFile(title, persons, options);.download() triggers a browser download. .toString() returns the GEDCOM text.
import { ExportGedcomFile, type ReadGed } from "gedcom-ts";
function exportGed(readGed: ReadGed) {
new ExportGedcomFile("my-tree", readGed.persons, {
extraTopLevelRecords: readGed.preservedTopLevelRecords,
objeRecordsById: readGed.objeRecordsById,
headLanguageTag: "fr-FR",
headCopyright: "© 2026 Family Archive",
headDestination: "https://gedcom.io/",
headSchemaTagDefs: [{ tag: "_FOO", uri: "https://example.com/foo" }],
}).download();
}ExportGedzipFile
Writes a .zip (GEDZIP) bundling the GEDCOM and all attached MultimediaFile payloads.
import { ExportGedzipFile, Person } from "gedcom-ts";
async function exportZip(persons: Person[]) {
await new ExportGedzipFile("my-tree", persons).download();
}GedcomExportOptions
Options shared by both exporters:
| Option | Effect |
| ---------------------- | -------------------------------------------------------------------------------------------------------- |
| extraTopLevelRecords | Raw 0 … blocks re-emitted before SUBM / TRLR (round-trip with readGed.preservedTopLevelRecords). |
| objeRecordsById | Full OBJE records (multi-FILE, MEDI AUDIO, TRAN) from readGed.objeRecordsById. |
| headLanguageTag | BCP 47 tag for HEAD.LANG (defaults to en-US). |
| headCopyright | One-line 1 COPR notice. |
| headDestination | Value of HEAD.DEST (target app / URI). |
| headSchemaTagDefs | Extension tag definitions emitted as HEAD.SCHMA / 2 TAG …. |
Domain model
Person, Sex
import { Person, Sex } from "gedcom-ts";
const person = new Person();
person.INDI = 1;
person.SEX = Sex.M; // M | F | U | X
person.firstnames = ["Jean"];
person.lastname = "DUPONT";
person.addMultimedia(/* MultimediaFile */);
person.deleteMultimedia("1/photo.jpg");Key fields: INDI, sosa, SEX, firstnames, lastname, FAMC, FAMS, acts, notes, multimediaFiles, nameVariants, attributes.
PersonGedcomImportOptions (type)
Optional hints consumed by Person.createPersonJson when re-parsing a single individual block (label-keyed pointers for FAMS / FAMC / NOTE, and standalone OBJE payloads). Useful when assembling a graph manually outside ReadGed.
PersonNameVariant, PersonNameTranslation
Lossless representation of every 1 NAME block of an individual (type, NPFX/GIVN/SURN/… parts, TRAN translations). Person.nameVariants keeps them in order so alias / translation data survive an import → export round-trip.
import { PersonNameVariant, PersonNameTranslation } from "gedcom-ts";
const variant = new PersonNameVariant("Jean /Dupont/", "BIRTH");
variant.parts.push({ level: "2", tag: "GIVN", value: "Jean" });
variant.translations.push(new PersonNameTranslation("ジャン /デュポン/", "jp"));IndiAttribute, IndiGedcomSubLine
Generic level-1 individual attributes (FACT, DSCR, CAST, EDUC, OCCU, RELI, TITL, RESN, …) with their sub-lines preserved (IndiGedcomSubLine = { level; tag; value }).
import { IndiAttribute } from "gedcom-ts";
const occ = new IndiAttribute("OCCU", "Blacksmith", [
{ level: "2", tag: "DATE", value: "1820" },
]);Act, Acts, TypeAct, ActConstructionOptions
Act models an individual or family event (BIRT, MARR, etc.). Acts is the ordered collection on a Person. TypeAct is the union of every supported GEDCOM 7 event tag (= Gedcom7EventTag). Event-level notes live on act.notes (2 NOTE / 3 CONT in GEDCOM); they are distinct from person.notes (1 NOTE on INDI) and round-trip through ZIP export/import.
import { Act, Acts, Identifier, type TypeAct } from "gedcom-ts";
const acts = new Acts();
const type: TypeAct = Identifier.BIRT;
acts.add(new Act(type));
acts.sortByDate();ActConstructionOptions controls the extras when building an Act manually:
import { Act, DateAct, Identifier } from "gedcom-ts";
const act = new Act(
Identifier.EVEN,
new DateAct("12 JAN 1901"),
null,
null,
null,
undefined,
undefined,
{
evenDescription: "Won a medal",
evenTypeLabel: "Award",
sdateAct: new DateAct("13 JAN 1901"),
eventPhrases: ["family gathering"],
preservedSubrecordPrefix: [],
preservedSubrecordSuffix: [],
},
);GEDCOM_7_ALL_EVENT_TAGS, GEDCOM_7_EVENT_SORT_ORDER, GEDCOM_7_EVENT_TAG_SET, GEDCOM_7_PAIR_UNION_EVENT_TAGS, Gedcom7EventTag, Gedcom7PairUnionEventTag
Canonical lists of GEDCOM 7 event tags (INDIVIDUAL_EVENT_STRUCTURE ∪ FAMILY_EVENT_STRUCTURE, LDS ordinances excluded). Use them to build UI selects or guard custom logic. GEDCOM_7_PAIR_UNION_EVENT_TAGS lists tags commonly used when creating a family union via createMarriageFamily (ENGA, MARB, MARC, MARL, MARR, MARS).
import {
GEDCOM_7_ALL_EVENT_TAGS,
GEDCOM_7_EVENT_SORT_ORDER,
GEDCOM_7_EVENT_TAG_SET,
type Gedcom7EventTag,
} from "gedcom-ts";
const options: Gedcom7EventTag[] = [...GEDCOM_7_ALL_EVENT_TAGS];
const isEvent = GEDCOM_7_EVENT_TAG_SET.has("MARR");
const sorted = [...GEDCOM_7_EVENT_SORT_ORDER];DateAct, Day, Month, dateToDateLine, days, months, TypeDateActSimpleQualifier
GEDCOM dates with full GEDCOM 7 / 5.5 support, including INT, EST, CAL, BET … AND …, FROM … TO …, ISO YYYY-MM-DD, 3 PHRASE under DATE, 3 TIME, and a verbatim fallback for unparseable payloads.
import {
DateAct,
Day,
Month,
dateToDateLine,
days,
months,
} from "gedcom-ts";
const day: Day = 12;
const month: Month = months[0]; // JAN
const dateLine = dateToDateLine(1901, month, day); // "12 JAN 1901"
const dateAct = new DateAct(dateLine);
const formattedDate = dateAct.date;
const knownDaysCount = days.length;TypeDateActSimpleQualifier is the union of single-anchor qualifiers (BEF | ABT | AFT | INT | EST | CAL) accepted by DateAct.updateQualifiedDate and by DateActEdit.setQualified.
Place, CoordinateGPS
import { Place, CoordinateGPS } from "gedcom-ts";
const place = new Place("Paris, FR", new CoordinateGPS(48.8566, 2.3522));
place.setFromGedcom7PlacPayload("Paris, Île-de-France, France");
const payload = place.toGedcom7PlacPayload(); // "Paris, , Île-de-France, France"Place understands the City, County, State, Country GEDCOM 7 list (1 to 4+ segments) and exposes placPhrase for 3 PHRASE under PLAC (import + export).
MultimediaFile, MultimediaFiles
Wraps either a local File, an external URI (sourceUri) or an OBJE pointer (objeXrefId) so the same model can survive a GED / ZIP round-trip. See Audio and oral history for interview workflows.
import { MultimediaFile, MultimediaFiles } from "gedcom-ts";
const bucket = new MultimediaFiles();
bucket.relativePath = "1/BIRT";
bucket.add(new MultimediaFile(new File(["img"], "birth.jpg")));
const remote = new MultimediaFile(undefined, "https://example.com/p.jpg");
const isExportable = remote.hasExportablePayload();Note, Notes, TypeNote
import { Notes, Note, Identifier, type TypeNote } from "gedcom-ts";
const typeNote: TypeNote = Identifier.CONT;
const note = new Note();
note.updateType(typeNote);
note.updateLines(["first line", "second line"]);
const notes = new Notes();
notes.addNote(note);
notes.removeFromIndex(0);Identifier (and deprecated Identificator)
Enum of every GEDCOM tag the library refers to (INDI, BIRT, MARR, DATE, PLAC, …). Prefer Identifier; Identificator is kept as a deprecated alias.
import { Identifier } from "gedcom-ts";
const birthTag = Identifier.BIRT;EventsByYears, ActsByYear
Group acts by year, deduplicating per individual. Handy to build chronological timelines.
import { EventsByYears, ActsByYear } from "gedcom-ts";
const grouped = new EventsByYears(person.acts.list);
for (const bucket of grouped.events) {
console.log(bucket.year, bucket.list.length);
}
const yearBucket = new ActsByYear(1901);Edit layer (in-place, chainable)
Wrap an existing model object to mutate it with a fluent API. Every method returns this, so you can chain. .value exposes the underlying target.
import { editPerson, editDateAct, DateAct } from "gedcom-ts";
editPerson(person)
.setLastname("Dupont")
.setFirstnames(["Jean", "Marie"])
.acts()
.at(0)
.setDateAct(new DateAct("1 JAN 1900"));
editDateAct(person.acts.list[0].dateAct!).setExactDate(1900, "JAN", 1);| Helper / class | Purpose |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| editPerson(person) / PersonEdit | Identity, FAMS / FAMC / removeFams*, nameVariants() / attributes() / multimedia(), bulk clear*, entry points to acts() and notes(). |
| editActs(acts) / ActsEdit | add, addNew, insertAt, insertNewAt, replaceAt, removeAt, removeAct, removeLast, clear, sortByDate, at, indexOfAct. |
| editAct(act) / ActEdit | setType, setIndis, dates, place, EVEN fields, preserved lines, notes(), multimedia(), asAudioInterview(), clearNotes, clearMultimedia. |
| editAudioInterviewAct(act) / AudioInterviewActEdit | Oral-history facade: attachAudio, setTranscription, setDescription, registerPendingObje, isAudioInterview, … |
| editDateAct(dateAct) / DateActEdit | clear, applyGedcomPayload, setExactDate, setQualified, setBetween, setFromTo, setTime, setDatePhrase / appendDatePhrase, setVerbatimPayload. |
| editPlace(place) / PlaceEdit | setFromGedcom7Payload, setCity, setCounty, setState, setCountry, setPlacPhrase / appendPlacPhrase / clearPlacPhrase, setCoordinates / clearCoordinates / replaceCoordinateModel, clearStructured. |
| editNotes(notes) / NotesEdit | add, addNew, insertAt, insertNewAt, replaceAt, removeAt, removeNote, removeLast, clear, at, indexOfNote. |
Dataset editing: ReadGed, graph, clone, export options, validation
Higher-level helpers complement the per-object edit facades:
import type { GedcomExportOptions } from "gedcom-ts";
import {
createEmptyReadGed,
createPersonStub,
DateAct,
Sex,
editReadGed,
editGedcomExportOptions,
clonePerson,
cloneAct,
createMarriageFamily,
linkChildToFamily,
removeFamilyReferencesFromDataset,
addPersonToReadGed,
validateReadGed,
Identifier,
Act,
} from "gedcom-ts";
const readGed = createEmptyReadGed();
const p = createPersonStub(readGed.generateUniqueIndi(), { sex: Sex.M, firstnames: ["Jean"], lastname: "Dupont" });
editReadGed(readGed).addPerson(p);
p.acts.add(new Act(Identifier.BIRT, new DateAct("1900")));
editReadGed(readGed).preserved().append("0 @S42@ SOUR Custom");
const opts: GedcomExportOptions = {};
editGedcomExportOptions(opts)
.setHeadCopyright("© 2026")
.setExtraTopLevelRecords([...readGed.preservedTopLevelRecords]);
const twin = clonePerson(p, readGed.generateUniqueIndi());
const birthCopy = cloneAct(p.acts.list[0]!);
// const fam = createMarriageFamily(readGed, spouseA, spouseB, { dateAct: new DateAct("1 JAN 2000") }, { eventTag: Identifier.ENGA });
// linkChildToFamily(readGed, child, fam);
// removeFamilyReferencesFromDataset(readGed, fam);
validateReadGed(readGed, {
checkMarrParticipants: true,
checkFamcWithoutSpouses: true,
checkFamsWithoutSpouses: true,
checkDuplicateFamsEntries: true,
checkAncestorCycles: true,
});Commands (invariants before mutation)
tryAddPersonToReadGed, tryRemovePersonFromReadGedByIndi, tryLinkChildToFamily, tryUnlinkChildFromFamily, and tryCreateMarriageFamily return a CommandResult (ok + issues or value + optional warnings). editReadGed(...).addPerson uses these checks internally (throws on blocking errors). For UI or transactional flows, prefer the try* APIs and inspect commandBlockingIssues.
| Export | Role |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| editReadGed(readGed) / ReadGedEdit | addPerson, removePersonByIndi, preserved() → PreservedTopLevelEdit (append, insertAt, replaceAt, removeAt, clear) on preservedTopLevelRecords. Low-level helpers: addPersonToReadGed, removePersonFromReadGedByIndi. |
| editGedcomExportOptions(opts) / GedcomExportOptionsEdit | Fluent setters for extraTopLevelRecords, objeRecordsById, headLanguageTag, headCopyright, headDestination, headSchemaTagDefs. |
| clonePerson(person, newIndi) / cloneAct(act) / person.clone / act.clone | Deep copies for templates or undo stacks. |
| nextFamilyId(persons) | Next internal family id F. |
| createMarriageFamily, linkChildToFamily, unlinkChildFromFamily, removeFamilyReferencesFromDataset | Create family F and spouse acts; optional { eventTag } (default MARR, also ENGA, banns, contract, EVEN + CreateActInit, …). See GEDCOM_7_PAIR_UNION_EVENT_TAGS. |
| validateReadGed, assertReadGedConsistent, validatePerson, assertPersonConsistent | Typed code / severity (error | warn). Options: checkMarrParticipants, checkDuplicateIndis, checkFamcWithoutSpouses, checkFamsWithoutSpouses, checkDuplicateFamsEntries, checkAncestorCycles. Assertions: failOn (default: error). |
| tryAddPersonToReadGed, tryRemovePersonFromReadGedByIndi, tryLinkChildToFamily, tryUnlinkChildFromFamily, tryCreateMarriageFamily, commandBlockingIssues | Command layer with CommandResult / validate*Command prechecks. |
Utilities
createSosaMap(root, partnersMap)
Computes a Sosa numbering (Ahnentafel) starting at root (Sosa 1) and recursively walking ancestors via partnersMap.
import { createSosaMap, Person } from "gedcom-ts";
const sosaMap = createSosaMap(root, readGed.partnersMap);
// e.g. sosaMap.get(root.INDI) === 1remainingTypesAct(acts, actToUpdate?)
Returns the list of event types still allowed for a person’s Acts, enforcing the “unique per individual” rule for BIRT / DEAT / BURI / CHR while keeping every other tag selectable. Pass the currently edited act as actToUpdate so its own type stays in the list when re-opening a form.
import { remainingTypesAct, Acts } from "gedcom-ts";
const available = remainingTypesAct(new Acts());getCityCoordinates(cityName, callback) (deprecated)
Legacy callback API. Use Geocoding places instead.
resolveDatasetVersion(headerLines) / GedcomDatasetVersion
Inspects the lines of a 0 HEAD block and returns "7.0", "5.5" or "unknown". Useful to branch UI behaviour for legacy datasets.
import { resolveDatasetVersion } from "gedcom-ts";
const version = resolveDatasetVersion(headLines); // "7.0" | "5.5" | "unknown"guessMediaFormFromUri(uri)
Best-effort media type detection from a path/URI or local File (file.type when set). Covers images, audio (mp3, webm, ogg, wav, m4a, flac, …), video, WebVTT, PDF. Used to fill OBJE.FILE.FORM. Pair with guessMediFromForm for MEDI (AUDIO, PHOTO, …). refineGenericObjeForm upgrades legacy application/octet-stream exports using the file extension.
import { guessMediaFormFromUri, guessMediaFormFromFile } from "gedcom-ts";
guessMediaFormFromUri("media/recording.webm"); // "audio/webm"
guessMediaFormFromFile(new File(["x"], "take.webm", { type: "audio/webm" })); // "audio/webm"extractPersonNameVariants(personLines) / extractIndiAttributes(personLines)
Low-level parsers used by Person.createPersonJson. They turn the raw GEDCOM lines of a single INDI record into structured PersonNameVariant[] / IndiAttribute[]. Reuse them when parsing custom GEDCOM fragments outside ReadGed.
import { extractPersonNameVariants, extractIndiAttributes } from "gedcom-ts";
const variants = extractPersonNameVariants(rawIndiLines);
const attributes = extractIndiAttributes(rawIndiLines);selectPrimaryNameVariant(variants)
Picks the most relevant 1 NAME block: priority to 2 TYPE BIRTH, then to a name with a /surname/ payload, otherwise the first variant.
import { selectPrimaryNameVariant } from "gedcom-ts";
const primary = selectPrimaryNameVariant(person.nameVariants);GEDCOM_LIBRARY_VERSION
CalVer string written in exported HEAD.SOUR.VERS (same value as the npm package version, e.g. 2026.6.0).
import { GEDCOM_LIBRARY_VERSION } from "gedcom-ts";
console.log(`gedcom-ts ${GEDCOM_LIBRARY_VERSION}`);Error handling
import { importGedFile, IMPORT_ERR_ZIP_GED_UNREADABLE } from "gedcom-ts";
try {
const readGed = await importGedFile(file);
console.log(readGed.persons.length);
} catch (error) {
if (error instanceof Error && error.message === IMPORT_ERR_ZIP_GED_UNREADABLE) {
console.error("Encrypted ZIP: please extract the .ged manually.");
} else {
console.error("GED import failed:", error);
}
}