TypeScript
The TypeScript generator turns a LinkML schema into TypeScript types for your data. The types
describe exactly the JSON that the generated JSON Schema describes, so they work with
JSON.parse and JSON.stringify. The output is a single .ts file:
- It has no runtime dependencies, no imports and no runtime code. It’s only types.
- It uses only syntax that TypeScript can erase (no
enum, nonamespace). So it also works with Node’s built-in type stripping and witherasableSyntaxOnly.
ohi get w3c/prov-oohi linkml to typescript --include-null \ --to prov-o.ts prov-o.linkml.yamlimport ohi_linkml
with ohi_linkml.load_file("prov-o.linkml.yaml") as schema: output = schema.typescript(include_null=True)import { readFileSync } from "node:fs";import { LinkML } from "@neverblink/linkml";
const yaml = readFileSync("prov-o.linkml.yaml", "utf8");const { view, report } = LinkML.loadFromString(yaml, {});if (!view) throw new Error(JSON.stringify(report));const output = view.typescript({ include_null: true });import eu.neverblink.linkml.generator.options.TypeScriptGeneratorOptionsimport eu.neverblink.linkml.generator.typescript.TypeScriptGeneratorimport eu.neverblink.linkml.schemaview.{SchemaIssues, SchemaView}
val loaded = SchemaView.loadSchemaViewFromUri("prov-o.linkml.yaml")given SchemaView = SchemaIssues.orThrow(loaded)val output = TypeScriptGenerator().serialize( TypeScriptGeneratorOptions(includeNull = true),)#include <stddef.h>#include "ohi_linkml.h"
linkml_init_threads();char *report = NULL, *error = NULL;long long schema = linkml_load_file("prov-o.linkml.yaml", NULL, &report, &error);char *output = linkml_typescript( schema, "{\"include_null\": true}", &error);linkml_free(output);linkml_free(report);linkml_free(error);linkml_close(schema);Options
Section titled “Options”error_handlingTypeScriptErrorHandlingdefault exception--error-handlingScala errorHandlingHow the functions of the schema report their errors.
exceptionThe method returns the value, and its doc comment gets a@throwstag for each error class.resultThe method returnsResult<R, Failure<...> | ...>: the value or one of the errors. A function with noerrorsreturnsRas it is.
include_nullbooleandefault false--include-nullScala includeNullWhether optional values and compact dictionary entries with nothing beyond the key may be null, as in the JSON Schema generator.
metadata_languagestringdefault en--metadata-languageScala metadataLanguageThe language of the titles and descriptions in the output, for schemas with text in more than one language.
openbooleandefault false--openWhether every interface allows properties beyond the ones in the schema, as additionalProperties in the JSON Schema generator. Interfaces of classes whose extra_slots allows them allow them anyway.
optional_defaultsbooleandefault false--optional-defaultsScala optionalDefaultsWhether a slot with an ifabsent may be left out, for data that a program fills the defaults in, such as options. Without it, such a slot is as required as the schema says, as in the JSON Schema generator.
pruning_modePruningModedefault skip--pruning-modeScala pruningModeThe classes and enums in the output. A designated union always comes with its members.
tree_rootThe elements reachable from the tree root class. Without a tree root, the same asschema.schemaEverything defined in the root schema, and every element reachable from it. This leaves out the unused parts of imported schemas, such as most oflinkml:types.skipEvery element of the root schema and of all its imports.
tree_rootstring--tree-rootScala treeRootThe class to use as the tree root, instead of the class with tree_root: true in the schema. Only for the tree_root pruning mode.
Loading and dumping data
Section titled “Loading and dumping data”Parse the JSON and tell TypeScript what it is:
import type { Zoo, Keeper } from "./model.ts";
const zoo = JSON.parse(text) as Zoo; // loadconst text2 = JSON.stringify(zoo, null, 2); // dumpThe cast is not checked at runtime. If the JSON doesn’t fit the schema, you find out only when
your code reads a field that isn’t what the type says. If the data comes from somewhere you
don’t trust, validate it first with the JSON Schema from to json-schema.
Example
Section titled “Example”This schema:
classes: Zoo: tree_root: true attributes: name: required: true animals: range: Animal multivalued: true inlined_as_list: true keepers: range: Keeper multivalued: true inlined: true Animal: abstract: true attributes: kind: designates_type: true required: true name: required: true keeper: range: Keeper Lion: is_a: Animal attributes: mane_length: range: float Parrot: is_a: Animal attributes: words: multivalued: true Keeper: description: A person who looks after animals. attributes: id: identifier: true name: since: range: date shift: range: Shiftenums: Shift: permissible_values: day: night:generates:
export type Animal = | Lion | Parrot;
/** A person who looks after animals. */export interface Keeper { id: string; name?: string; shift?: Shift; since?: string;}
export interface Lion { /** Reference to Keeper */ keeper?: string; kind: "Lion"; mane_length?: number; name: string;}
export interface Parrot { /** Reference to Keeper */ keeper?: string; kind: "Parrot"; name: string; words?: string[];}
export interface Zoo { animals?: Animal[]; keepers?: Record<string, KeyOptional<Keeper, "id">>; name: string;}
export type Shift = | "day" | "night";
/** The object `T`, with the key slot `K` optional, as used in dict values. */export type KeyOptional<T, K extends keyof T> = T extends unknown ? Omit<T, K> & Partial<Pick<T, K>> extends infer O ? { [P in keyof O]: O[P] } : never : never;How the schema maps to TypeScript
Section titled “How the schema maps to TypeScript”Classes
Section titled “Classes”Every class becomes an interface. The interface lists all of the class’s slots, including
the ones it inherits through is_a and mixins. There’s no extends. TypeScript compares
objects by their shape, so a Lion still fits wherever its parent type is expected.
Property names are the JSON keys. They are the same as in the JSON Schema: the slot’s alias
if it has one, otherwise its name in snake_case. A key that isn’t a valid identifier is
quoted, e.g. "odd-name"?: string.
Slot types
Section titled “Slot types”| LinkML range | TypeScript |
|---|---|
string, date, datetime, time, uri, curie, uriorcurie, ncname and types based on them |
string |
integer, float, double, decimal |
number |
boolean |
boolean |
linkml:Any |
unknown |
a type with union_of |
a union of its members’ types, e.g. string | number |
any_of or exactly_one_of members with ranges |
a union of the ranges’ types, e.g. number | Person |
| an enum | the enum’s type |
| a class, not inlined | the type of the class’s identifier, with a “Reference to X” comment |
| a class, inlined | the class’s type |
| a class, inlined as a list | X[] |
| a class, inlined as a dict (compact form) | Record<string, KeyOptional<X, "id">> |
| a class, inlined as a dict (simple form) | Record<string, V | KeyOptional<X, "id">>, where V is the type of the value slot |
A multivalued slot of any other range becomes an array, e.g. string[], or (string | number)[]
for a union.
A union_of type lists its members in their declared order, with nested unions replaced by their
own members. Members that map to the same TypeScript type appear once, so union_of: [integer, float] is just number.
In the dict forms the key of each entry is the object’s identifier, so the identifier is
optional inside the object. KeyOptional<X, "id"> is X with id made optional. It is only
added to the file when it’s used.
With --include-null, optional properties also accept null (name?: string | null), the same
as the JSON Schema option.
The ifabsent of a slot is in its doc comment, such as @defaultValue "en". A required slot
with an ifabsent stays required, as in the JSON Schema, unless you use --optional-defaults.
A class whose extra_slots allows extra data gets turned into [key: string]: unknown. If the class has no
slots and the range of extra_slots is a type, it’s that type instead, such as [key: string]: string.
An enum becomes a union of its values: export type Shift = "day" | "night";. An enum without
permissible values (e.g. a dynamic enum) becomes string.
Classes and enums get PascalCase names. A name that starts with a digit or is reserved in
TypeScript gets a _ prefix (e.g. a class Record becomes _Record).