SkillAgentSearch skills...

Alge

Type safe library for creating Algebraic Data Types (ADTs) in TypeScript. 🌱

Install / Use

/learn @jasonkuhrt/Alge
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

alge 🌱

trunk npm version

Hey 👋, FYI here are some other TypeScript-first libraries I've created that might interest you:

@molt/command for building simple scripts and command lines.

TL;DR

Library for creating Algebraic Data Types in TypeScript. Pronounced "AL GEE" like the plant (or whatever it is). Schemas powered by Zod <3.

An ADT is built like so:

import { Alge } from 'alge'
import { z } from 'zod'

const Length = z.number().positive()

//           o---------- ADT Controller
//           |            o--------- ADT Builder
export const Shape = Alge.data(`Shape`, {
  Rectangle: {
    width: Length,
    height: Length,
  },
  Circle: {
    radius: Length,
  },
  Square: {
    size: Length,
  },
})

Building an ADT returns a controller. Controllers are an API for your data, like constructors and type guards. Constructed data is nothing special, just good old JavaScript POJOs.

//    o--------- Member Instance
//    |        o--------- ADT Controller
//    |        |     o-------- Member Namespace
//    |        |     |      o-------- Constructor
//    |        |     |      |
const circle = Shape.Circle.create({ radius: 50 })
// { _tag: 'Circle', radius: 50 }

const square = Shape.Square.create({ size: 50 })
// { _tag: 'Square', size: 5 }

if (Shape.Circle.is(circle)) {
  console.log(`I Am Circle`)
}

const circleForTheOutsideWorld = Shape.Circle.to.json(circle)
// '{ "_tag": "Circle", "radius": 50 }'

const squareFromTheOutsideWorld = Shape.Square.from.json({ _tag: 'Square', size: 10 })
// { _tag: 'Square', size: 10 }

You can infer the static types from the controller:

type Shape = Alge.infer<typeof Shape>

You can pattern match on your constructed data:

const shape = Math.random() > 0.5 ? circle : square
const result = Alge.match(shape)
  .Circle({ radius: 13 }, () => `Got an unlucky circle!`)
  .Circle((circle) => `Got a circle of radius ${circle.radius}!`)
  .Square({ size: 13 }, () => `Got an unlucky square!`)
  .Square((square) => `Got a square of size ${square.size}!`)
  .done()

You can create individual records when you don't need full blown ADTs:

import { Alge } from 'alge'
import { z } from 'zod'

const Circle = Alge.record(`Circle`, { radius: z.number().positive() })

This is just a taster. Places you can go next:

  1. Install and learn interactively (JSDoc is coming soon!)

  2. A formal features breakdown

  3. Code examples

  4. A simple introduction to Algebraic Data Types (for those unfamiliar)

  5. A video introduction if you like that format

    Video Cover

Contents

<!-- toc --> <!-- tocstop -->

Installation

npm add alge

Roadmap

There is no timeline but there are priorities. Refer to the currently three pinned issues.

Features At a Glance

  • Use a "builder" API to define ADTs
    • Use Zod for schema definition
    • Define one or more codecs
  • Use the "controller" API to work with data
    • Constructors
    • Type guards
    • Built in JSON codec
    • Automatic ADT level codecs (for codecs common across members)
  • Pattern match on data
    • Use tag matchers
    • Use value matchers

About Algebraic Data Types

Alge is a Type Script library for creating Algebraic Data Types (ADTs). This guide will take you from not knowing what ADTs are to why you might want to use Alge for them in your code.

What?

Algebraic Data Types (ADTs for short) are a methodology of modelling data. They could appear in any context that is about defining and/or navigating the shape of data. One of their fundamental benefits is that they can express different states/inrecords/facts about/of data. They are the combination of two other concepts, product types and union types.

A product type is like:

interface Foo {
  bar: string
  qux: number
}

A union type is like:

type Foo = 1 | 2 | 3

Basically, when the power of these two data modelling techniques are combined, we get something far greater than the sum of its parts: ADTs.

ADTs can particularly shine at build time. While dynamically typed programing languages ("scripting language", e.g. Ruby, JavaScript, Python, ...) can support ADTs at runtime, adding static type support into the mix increases the ADT value proposition. Then there are yet other more minor programing language features like pattern matching that if supporting ADTs make them feel that much more beneficial too.

References:

Why?

Now that we have some understanding of what ADTs are let's build some understanding about why we might want to use them. To do this we'll work with an example.

Let's say we want to accept some user input about an npm package dependency version pin. It might come in the form of an exact version or a range of acceptable versions. How would we model this? Let's start without ADTs and then refactor with them to appreciate the difference. Let's assume that input parsing has been taken care of and so here we're only concerned with structured data modelling.

interface Pin {
  isExact: boolean
  patch?: number
  minor?: number
  major?: number
  release?: string
  build?: string
  range?: Array<{
    operator: `~` | `>=` | `...` // etc.
    patch: number
    minor: number
    major: number
    release?: string
    build?: string
  }>
}

This data modelling is flawed. There is out-of-band information about important data relationships. release and build are legitimately optional properties but range patch minor major all depend on the state of isExact. When true then range is undefined and the others are not, and vice-versa. In other words these configurations of the data are impossible:

const pin = {
  isExact: true,
  patch: 1,
  minor: 2,
  major: 3,
  range: [
    {
      operator: `~`,
      patch: 1,
      minor: 0,
      major: 0,
    },
  ],
}
const pin = {
  isExact: false,
  patch: 1,
  minor: 2,
  major: 3,
}

While these are possible:

const pin = {
  isExact: true,
  patch: 1,
  minor: 2,
  major: 3,
}
const pin = {
  isExact: true,
  patch: 1,
  minor: 2,
  major: 3,
  release: `beta`,
}
const pin = {
  isExact: false,
  range: [
    {
      operator: `~`,
      patch: 1,
      minor: 0,
      major: 0,
    },
  ],
}

But since our data modelling doesn't encode these facts our code suffers. For example:

if (pin.isExact) {
  doSomething(pin.major!)
  //                       ^
}

Notice the !. Its us telling Type Script that major is definitely not undefined and so the type error can be ignored. In JS its even worse, as we wouldn't even be prompted to think about such cases, unless we remember to. Seems trivial in this case, but at scale day after day often with unfamiliar code a mistake will inevitably be made. Another approach could have been this:

if (pin.isExact) {
  if (!pin.major) throw new Error(`Bad pin data!`)
  doSomething(pin.major)
}

So, poor data modelling affects the quality of our code by our code either needing to deal with apparently possible states that are actually impossible OR by our code carefully ignoring those impossible states. Both solutions are terrible because they make code harder to read. There is more code, and the chance that wires about

Related Skills

View on GitHub
GitHub Stars126
CategoryDevelopment
Updated3mo ago
Forks5

Languages

TypeScript

Security Score

97/100

Audited on Dec 11, 2025

No findings