DEV Community

Cover image for Modeling JSON-LD in TypeScript: A Story in Four Parts

Posted on

Modeling JSON-LD in TypeScript: A Story in Four Parts

Recently, I published schema-dts, an open source library that models JSON-LD in TypeScript. A big reason I wanted to do this project is because I knew some TypeScript type system features, such as discriminated type unions, powerful type inference, nullability checking, and type intersections, present an opportunity to both model what JSON-LD looks like, while also providing ergonomic completions to the developer.

I wrote a series of posts, describing some of the Structured Data concepts that lent themselves well to TypeScript’s type system, and those concepts that didn’t.

1. Modeling Class Hierarchy using Structural Typing JSON-LD node objects are always typed (that is, they have a @type property that points to some IRI–a string–describing it). Given a @type you know all the properties that are defined on a particular object. Object types inherit from each other. For example, Thing in has a property called name, and Person is a subclass of Thing that defines additional properties such as birthDate, and inherits all the properties of Thing such as name. Thing has other sub-classes, like Organization, with it’s own properties, like logo.

In the first installment, we end up uncovering a recursive TypeScript inheritance hierarchy we can use to model the full complexity of class inheritance.

Recursive Hierarchy

2. Enumerations in TypeScript

When trying to model Enumerations, we looked at a ton of examples from the website to discover that absolute IRIs or @context-relative IRIs are expected to model the value of an enumeration. But we also found out that Enumerations can be arbitrary nodes, and take part in the class hierarchy.

Example of an Enum Hierarchy

3. DataType in TypeScript

The DataType hierarchy is far richer than TypeScript’s type system can accommodate. In the third installment, we figured out what trade-offs can we make.

4. Class Properties and Special Cases

Properties -- all the stuff that actually lives within a JSON node -- turn out to be more complicated than we thought: they're all optional, they're all repeated, they can supersede one another, and then can subclass one another.

The End Result

The end result is schema-dts itself. We can create programmatic TypeScript definitions that express much of For example, the top-level Thing type in can be represented as:

type ThingBase = {
    /** An additional type for the item, typically used for adding more specific types from external vocabularies in microdata syntax. This is a relationship between something and a class that the thing is in. In RDFa syntax, it is better to use the native RDFa syntax - the 'typeof' attribute - for multiple types. tools may have only weaker understanding of extra types, in particular those defined externally. */
    "additionalType"?: URL | URL[];
    /** An alias for the item. */
    "alternateName"?: Text | Text[];
    /** A description of the item. */
    "description"?: Text | Text[];
    /** A sub property of description. A short description of the item used to disambiguate from other, similar items. Information from other properties (in particular, name) may be necessary for the description to be useful for disambiguation. */
    "disambiguatingDescription"?: Text | Text[];
    /** The identifier property represents any kind of identifier for any kind of {@link Thing}, such as ISBNs, GTIN codes, UUIDs etc. provides dedicated properties for representing many of these, either as textual strings or as URL (URI) links. See {@link /docs/datamodel.html#identifierBg background notes} for more details. */
    "identifier"?: (PropertyValue | Text | URL) | (PropertyValue | Text | URL)[];
    /** An image of the item. This can be a {@link URL} or a fully described {@link ImageObject}. */
    "image"?: (ImageObject | URL) | (ImageObject | URL)[];
    /** Indicates a page (or other CreativeWork) for which this thing is the main entity being described. See {@link /docs/datamodel.html#mainEntityBackground background notes} for details. */
    "mainEntityOfPage"?: (CreativeWork | URL) | (CreativeWork | URL)[];
    /** The name of the item. */
    "name"?: Text | Text[];
    /** Indicates a potential Action, which describes an idealized action in which this thing would play an 'object' role. */
    "potentialAction"?: Action | Action[];
    /** URL of a reference Web page that unambiguously indicates the item's identity. E.g. the URL of the item's Wikipedia page, Wikidata entry, or official website. */
    "sameAs"?: URL | URL[];
    /** A CreativeWork or Event about this Thing.. */
    "subjectOf"?: (CreativeWork | Event) | (CreativeWork | Event)[];
    /** URL of the item. */
    "url"?: URL | URL[];
/** The most generic type of item. */
export type Thing = ({
    "@type": "Thing";
} & ThingBase) | (Action | CreativeWork | Event | Intangible | MedicalEntity | Organization | Person | Place | Product);

View the entire series at

Top comments (2)

shnydercom profile image
Jonathan Schneider • Edited

I'm really happy I found this library now! I've been working on a lowcode-editor for frontends, with the application state using (at runtime). Since n-to-n relationships seem like the default for properties, and almost anything can relate to any Thing, I admire your work (and will use your library)! :) Thanks for building and sharing this!

johnsonjo4531 profile image
John Johnson

Thanks so much for this library @Eyas!