tsum
TypeScript icon, indicating that this package has built-in type declarations

0.3.0 • Public • Published

tsum

Typescript sum types with pattern matching, using multimethods

Install

npm install --save tsum

yarn add tsum

Motivation

Being able to simply represent, manipulate, and pattern match on immutable values is one of the most important things a programming language can provide.

Typescript offers two built-in solutions:

  1. Add a 'type' field to all your data, and switch on it. This is verbose, requires explicit serialize/deserialize, and error prone when specifying literal data. Additionally, switch offers no exhaustiveness guarantee, and is a statement rather than an expression.
  2. Put your data into classes. This is verbose, requires explicit serialize/deserialize and lacks named fields:
    new Person('Garrett') vs {name: 'Garrett'}
    Most importantly, you lose the ability to easily make new immutable values from previous values.

Using plain data structures solves many of these problems, and improves notation:

  • Values are trivial to persist or send over a wire, and trivial to read when receiving data from the wire or disk
  • Data definitions are as terse and straightforward as possible in Typescript
  • Fields are named
  • Open system, as long as a piece of data matches one of these interfaces, it can participate in this polymorphism (unlike classes)
  • Matching every possible case is enforced by the compiler, and matches are expressions which return a value
  • Critically, you gain the ability to use spread notation to flexibly produce new values:
    const olderPerson = {...person, age: person.age + 1}
    Classes offer no equivalent to this

Example

import Sum from 'tsum'

Describe each of your types:

interface Dog {
  name: string
  color: string
}

interface Cat {
  name: string 
  dogFriendly: boolean
}

interface Chicken {
  eggsLaid: number
}

interface Cow {
  mooVolume: number
}

Put them together:

interface Animals {
  Dog: Dog
  Cat: Cat
  Chicken: Chicken
  Cow: Cow
}

Provide the Animals interface, along with a set of predicates to determine the type of an arbitrary Animal:

const Animal = Sum<Animals>({
  predicates: ({isString, isBoolean, isNumber}) => ({
    Dog:     _ => isString(_.color),
    Cat:     _ => isBoolean(_.dogFriendly),
    Chicken: _ => isNumber(_.eggsLaid),
    Cow:     _ => isNumber(_.mooVolume)
  })
})

Create functions that act on any Animal:

// The type of describe is: (animal: Animal) => string
const describe = Animal.f({
  // Typescript knows the type in each branch, and makes sure you cover all cases
  Dog: dog => `a ${dog.color} dog named ${dog.name}`,
  Cat: cat => cat.dogFriendly ? `a cat named ${cat.name} (dog-friendly)` : `a cat named ${cat.name} (not dog-friendly)`,
  Chicken: chicken => `a chicken who has laid ${chicken.eggsLaid} eggs`,
  Cow: cow => cow.mooVolume > 5 ? 'a loud cow' : 'a quiet cow'
})

const dog = {name: 'Zorro', color: 'grey'}
const cat = {name: 'Oberyn', dogFriendly: false}
const chicken = {eggsLaid: 13}
const cow = {mooVolume: 6}

console.log(describe(dog))     // a grey dog named Zorro
console.log(describe(cat))     // a cat named Oberyn (not dog-friendly)
console.log(describe(chicken)) // a chicken who has laid 13 eggs
console.log(describe(cow))     // a loud cow

Or match inline:

const cat = {name: 'Oberyn', dogFriendly: false}
const cost = Animal.match(cat, {
  Dog: dog => dog.name === 'Zorro' ? 99999 : 300,
  Cat: cat => cat.name === 'Oberyn' ? 99999 : 800,
  Chicken: chicken => chicken.eggsLaid * 10,
  Cow: cow => cow.mooVolume > 5 ? 5000 : 6000
})
console.log(cost) // 99999

Convenience to avoid repeating yourself:

// Evaluates to Dog | Cat | Chicken | Cow
type Animal = typeof Animal.types

Thanks to:

The Value of Values

Readme

Keywords

none

Package Sidebar

Install

npm i tsum

Weekly Downloads

4

Version

0.3.0

License

ISC

Last publish

Collaborators

  • garrettm