Skip to content
/ tsum Public

Typescript sum types with pattern matching, using multimethods

Notifications You must be signed in to change notification settings

garrettm/tsum

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

About

Typescript sum types with pattern matching, using multimethods

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published