npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

typesafe-match

v1.2.0

Published

Match safely on algrebraic data types

Downloads

5

Readme

typesafe-match

Exhaustive match on algebraic data types.

npm i typesafe-match

Usage

typesafe-match is intended for use with Typescript.

Let's start with a simple toy model :

interface Dog {
  name: string
  age: number
}

interface Cat {
  name: string
  age: number
  cutenessLevel: number
}

type Animal = Cat | Dog

Animal is our ADT while Dog and Cat are its components.

Each interface should extends Product and pass it its name :

import type { Product } from 'typesafe-match' 

interface Dog extends Product<"Dog"> { ... }
interface Cat extends Product<"Cat"> { ... }

type Animal = Cat | Dog

Each type of the ADT should request a type aware Constructor by using the macro of the same name.

import { Constructor } from 'typesafe-match'

...

const Dog = Constructor<Dog>()
const dodger = Dog("Dodger", 23)

One can then build the matcher for this ADT :

import { Match } from 'typesafe-match'

...

const matchAnimal = Match<Animal>()

matchAnimal can be used as simple function anywhere. It will check for exhaustiveness at compile time.

const dodger: Animal = Dog("Dodger", 23)

const nameOrNot = matchAnimal(dodger)({ //Compile
  Dog: (dog: Dog) => dog.name,
  Cat: (cat: Cat) => "I am not a cat"
})

const nameOrNot = matchAnimal(dodger)({ //Does not compile, missing cases
  Dog: (dog: Dog) => dog.name
})

If you want to handle all remaining cases, a special key is defined for that:

const dodger: Animal = Dog("Dodger", 23)

const nameOrNot = matchAnimal({
  Dog: (dog: Dog) => dog.name,
  _: () => "still not a cat" //Will match all non-dog cases, only cat for this example
})(dodger)

Installation

To use typesafe-match you need to compile against its macro.

You can do that by defining devDependencies and changing the compilation step you use. A simple example can be found in the package.json of this repositories. All optionalDependencies of this package should be fullfiled (as devDependencies not runtime ones) and a special element should be added to your install script.

Chaining

Finally, the matchAnimal function is curried to be of simple use in reactive chains:

const animals = ???

animals.filter(matchAnimal({
  Dog: () => true,
  Cat: (cat: Cat) => cat.name != "miaous"
})).map(matchAnimal({
  Dog: () => "I'm a good boy"
  Cat: () => "I'm not miaous"
}))

but It can also be used normally :

const animal = ???

matchAnimal(animal)({
  Dog: () => true,
  Cat: (cat: Cat) => cat.name != "miaous"
}))

Or the two can be mixed together :

const animal = ???
const catFriends = ???
const dogFriends = ???

matchAnimal(animal)({
  Dog: () => catFriends,
  Cat: () => dogFriends
})).map(matchAnimal({
  Dog: (dog: Dog) => `I'm the cat's friend ${dog}`
  Cat: (cat: Cat) => `I'm some big and slow dog's friends ${cat}`
}))

To select our best friend friend's !

Embracing Complexity

A matcher can also use sub-matchers and descriptors for more complexe cases. It still remains typesafe as, if you provide an incomplete list of descriptors and sub-matchers, it will refuse to compile.


const animal = ???

const youngest = matchAnimal(animal)({
  Cat: oneOf(
    matchCase({name: 'feul', cutenessLevel: 9999})((_: Cat) => 0),
    defaultCase((cat: Cat) => cat.age)
  ),
  Dog: (dog: Dog) => dog.age,
})

Here the sub-matcher describe one particular cat and, as we prefer it, set its age to 0, because it deserved to win. We use the matchCase function to describe the object we want to match. If animal does have this two properties then matchAnimal will choose this line and use the 0 trick to have our cat win.

It's also possible to describe all our cases, without defining a default.


// For this example sake, dont do this at home
interface Cat {
  name: string
  age: number | number
}

const animal = ???

const youngest = matchAnimal(animal)({
  Cat: oneOf(
    matchCase({ name: 'feul' })((_: Cat) => 0),
    matchCase({ name: 'schnee', age: number() })((schnee: Cat) => (schnee.age as number) - 5),
    matchCase({ age: string() })((cat: Cat) => parseInt(cat.age)),
    matchCase({ name: string() })((cat: Cat) => cat.age as number)
  ),
  Dog: (dog: Dog) => dog.age,
})

In this little more involved example we have two awesome cats. feul is still our desired winner, so we set it's age to 0 again. But if we have a cat named schnee and its age is a number then we want to give it a 5 years advantage. If we dont have these two cats, but we do have a cat with an age being a string then we want to parse it, finally the last case only specifies that the cat need to be named, it's aged will be a number as we already handle all others cats above.

If we forgot to specify one of the cases, except the first that is too specific to matter, we get a compilation error.


interface Cat { ... }

const animal = ???

const youngest = matchAnimal(animal)({
  Cat: oneOf( // Wont compile, missing the case where the Cat has a number's age but is not schnee.
    matchCase({ name: 'feul' })((_: Cat) => 0),
    matchCase({ name: 'schnee', age: number() })((schnee: Cat) => (schnee.age as number) - 5),
    matchCase({ age: string() })((cat: Cat) => parseInt(cat.age))
  ),
  Dog: (dog: Dog) => dog.age,
})

Design Limitation

To obtains compile time exhaustivity checks some design decisions have been made. The most visible part is that you need to pass the name of your interfaces as string to Product. It will stay necessary as long as Typescript does not provide type-level operations returning the name of a given type.

The choice of the _ character as remaining cases is a personal preference but should appear clear to most people. To avoid clashes in the exhaustivity checks you should never name one of your interface _. I do believe it's not a problem for most.