Hacker News new | ask | show | jobs
by jitl 628 days ago
(I work at Notion)

Our public API format is not the "native" format used by the Notion editor. The overly-nested format we designed the public API is aimed at supporting statically typed programming languages like Java or Golang that do not have (tagged) union types natively. Instead we represent each option in the union type as a nullable field pointing to a nested object. This makes the structure much easier to decode in these kinds of languages, but does make it more verbose.

For a hypothetical typescript union type:

    | { type: 'plain', text: string, format: Formatting }
    | { type: 'link', text: string, href: string, format: Formatting }
    | { type: 'page-mention', page: { id: UUID, spaceId: UUID }, format: Formatting }
we end up producing a Java-style object like this:

    {
      plain?: {
        text: string,
        format: Formatting
      }
      link?: {
        text: string,
        href: string,
        format: Formatting
      }
      pageMention?: {
        page: {
            id: UUID,
            spaceId: UUID
        },
        format: Formatting
      }
    }
You can see the native format by looking at API traffic in your browser devtools. Generally the native format is more confusing without type annotations.
1 comments

Switching an enum on a `type` field is very much doable and common practice in statically typed languages.

Rust's serde calls that "internally tagged": https://serde.rs/enum-representations.html

I specifically said "statically typed programming languages like Java or Golang" because those are languages without constructs that can easily represent something like a Typescript union-of-objects. There are many statically typed programming languages that do support union types!

Rust structured enums qualify - each enum case can have fields, so it's natural to represent a Typescript union type using a Rust enum; It's not quite the same (needs a newtype wrapper for each different combination of types in the union) but is very serviceable for the API use-case.

C and Zig have a different "union" concept where the caller needs to switch on a "tag" field like `type` in Typescript but with less compiler guidance to ensure you use the right union variant for a certain tag value.

Java and Go are more limited. Java can represent an "internally tagged" union as a superclass with subclasses, and Go can represent as an interface type with a method for getting the variant, but both require the consumer to know all the subtypes, and perform their own "if canCast(supertypeValue, unionCaseSubclass1) { // handle case 1 } else if ..." structure, which substantially hinders discoverability and requires more work.