0.8.0-rc.2 • Published 4 years ago

morphic-ts v0.8.0-rc.2

Weekly downloads
249
License
MIT
Repository
github
Last release
4 years ago

Morphic-ts

Business Models just got a lot easier

This library adress the pain of writting and maintaining code for business without any Magic in Typescript

The goal is to increase, in order of importance

  • Correctness
  • Productivity
  • Developper Experience

It is has two side blended into one; generic ADT manipulation AND Generic, customizable and extensible derivations

Two minutes intro

npm install 'morphic-ts'

Or

yarn add 'morphic-ts'

Then

import { summon } from 'morphic-ts/lib/batteries/summoner'

export const Person = summon(F =>
  F.interface(
    {
      name: F.string(),
      age: F.number()
    },
    'Person'
  )
)

// You now have acces to everything to develop around this Type
Person.build // basic build function (enforcing correct type)
Person.show // Show from fp-ts
Person.type // io-ts
Person.strictType // io-ts
Person.eq // Eq from fp-ts
Person.lenseFromPath // and other optics (optionnals, prism, ) from monocle-ts
Person.arb // fast-check
Person.jsonSchema // JsonSchema-ish representation

Discriminated, taggedUnion-like models

import { summon, tagged } from 'morphic-ts/lib/batteries/summoner-no-union'

export const Bicycle = summon(F =>
  F.interface(
    {
      type: F.stringLiteral('Bicycle'),
      color: F.string()
    },
    'Bicycle'
  )
)

export const Car = summon(F =>
  F.interface(
    {
      type: F.stringLiteral('Car'),
      kind: F.keysOf({ electric: null, fuel: null, gaz: null }),
      power: F.number()
    },
    'Car'
  )
)

const Vehicule = tagged('type')({ Car, Bicycle })

// Now you have access to previously depicted derivation + ADT support (ctors, predicates, optics, matchers,reducers, etc.. see `ADT Manipulation` below)

Want opaque nominal (instead of structural) infered types

You may use this pattern

const Car_ = summon(F =>
  F.interface(
    {
      type: F.stringLiteral('Car'),
      kind: F.keysOf({ electric: null, fuel: null, gaz: null }),
      power: F.number()
    },
    'Car'
  )
)
export interface Car extends AType<typeof Car_> {}
export interface CarRaw extends EType<typeof Car_> {}
export const Car = AsOpaque<CarRaw, Car>(Car_)

We're sorry for the boilerplate, this is a current Typescript limitation but in our experience, this is worth the effort.

Configurable

As nice as a General DSL solution to specify your Schema is, there's still some specifics you would like to use.

Morphic provides Interpreter to expose Config for a specific Algebra combinator.

For example, we may want to specify how fastcheck should generate some arrays. We can add an extra parameter to a definition (last position) and use Interpreter specific function (named 'InterpreterConfig', here fastCheckConfig) and it will expose the ability to specify the configuration for this Interpreter and combinator.

summon(F => F.array(F.string(), fastCheckConfig({ minLength: 2, maxLength: 4 })))

Note: this is type guided and type safe, it's not an any in disguise

You may provide several Configuration

summon(F => F.array(F.string(), { ...fastCheckConfig({ minLength: 2, maxLength: 4 }), ...showConfig(...)} ))

How it works

When you specify a Schema, you're using an API (eDSL implemented using final tagless). This API defines a Program (your schema) using an Algebra (the combinators exposed to do so).

This Algebra you're using is actually composed of several Algebras merged together, some defines how to encode a boolean, some others a strMap (string Map), etc..

Then for each possible derivation there's possibly an Ìnterpreter` implementing some Algebras. What Morphic does is orchestrating this machinery for you

This pattern has some interesting properties; it is extensible in both the Algebra and the Interpreter

Generic Derivation

Specify the structure of your Schema only once and automatically has access various supported implementations

Participate into expanding implementation and/or schema capabilities

Example of implementations:

  • Structural equality (via Eq from fp-ts)
  • Validators (io-ts)
  • Schema generation (JsonSchema flavor)
  • Pretty print of data structure (Show from fp-ts)
  • Generators (FastCheck)
  • ...
  • TypeOrm (WIP)

This is not an exhaustive list, because the design of Morphic enables to define more and more Interpreters for your Schemas (composed of Algebras).

ADT Manipulation

ADT stands for Algebraic Data Types, this may be strange, just think about it as the pattern to represent your casual Business objects

ADT manipulation support maybe be used without relying on full Morphic objects.

The feature can be used standalone via the makeADT function with support for:

  • Smart Ctors
  • Predicates
  • Optics (Arcane name for libraries helping manipulate immutable data structures in FP)
  • Matchers
  • Reducers
  • Creation of new ADTs via selection, exclusion, intersection or union of existing ADTs

Ad'hoc usage via makeADT (Morphic's summon already does that for you):

Let's define some Types

interface Bicycle {
  type: 'Bicycle'
  color: string
}

interface Motorbike {
  type: 'Motorbike'
  seats: number
}

interface Car {
  type: 'Car'
  kind: 'electric' | 'fuel' | 'gaz'
  power: number
  seats: number
}

Then build an ADT from them for PROFIT!

// ADT<Car | Motorbike | Bicycle, "type">
const Vehicle = makeADT('type')({
  Car: ofType<Car>(),
  Motorbike: ofType<Motorbike>(),
  Bicycle: ofType<Bicycle>()
})

Then you have..

Constuctors

Vehicle.of.Bicycle({ color: 'red' }) // type is Car | Motorbike | Bicycle

// `as` offer a narrowed type
Vehicle.as.Car({ kind: 'electric', power: 2, seats: 4 }) // type is Car

Predicates

// Predicate and Refinements
Vehicle.is.Bicycle // (a: Car | Motorbike | Bicycle) => a is Bicycle

// Exist also for several Types
const isTrafficJamProof = Vehicle.isAnyOf('Motorbike', 'Bicycle') // (a: Car | Motorbike | Bicycle) => a is Motorbike | Bicycle

Matchers

const nbSeats = Vehicle.match({
  Car: ({ seats }) => seats,
  Motorbike: ({ seats }) => seats,
  Bicycle: _ => 1
})

// Alternatively you may use `default`
Vehicle.match({
  Car: ({ seats }) => seats,
  Motorbike: ({ seats }) => seats,
  default: _ => 1
})

// Use matchWiden, then the resturn type will be unified from each results
// Here it would be number | 'none'
Vehicle.matchWiden({
  Car: ({ seats }) => seats,
  Motorbike: ({ seats }) => seats,
  default: _ => 'none' as const
})

Transformers

// You may tranform matching a subset
Vehicle.transform({
  Car: car => ({ ...car, seats: car.seats + 1 })
})

Reducers

// Creating a reducer is made as easy as specifying a type
Vehicle.createReducer({ totalSeats: 0 })({
  Car: ({ seats }) => ({ totalSeats }) => ({ totalSeats: totalSeats + seats }),
  Motorbike: ({ seats }) => ({ totalSeats }) => ({ totalSeats: totalSeats + seats }),
  default: _ => identity
})

Selection, Exclusion, Intersection and Union of ADTs

This will help getting unique advantage of Typescript ability to refine Unions

const Motorised = Vehicle.select('Car', 'Motorbike') // ADT<Car | Motorbike, "type">

const TrafficJamProof = Vehicle.exclude('Car') // ADT<Motorbike | Bicycle, "type">

const Faster = intersectADT(Motorised, TrafficJamProof) // ADT<Motorbike, "type">

const Faster = intersectADT(Motorised, TrafficJamProof) // ADT<Motorbike, "type">

const ManyChoice = unionADT(Motorised, TrafficJamProof) // ADT<Car  | Motorbike | Bicycle, "type">

Optics (via Monocle)

We support lenses, optionel, prism pretyped helpers

Lense example:

const seatLense = Motorised.lenseFromProp('seats') // Lens<Car | Motorbike, number>

const incSeat = seatLense.modify(increment) // (s: Car | Motorbike) => Car | Motorbike

Generic functions - Interepreters constraints Advanced usage

When one want to make a generic code, available to several interpreters, he can constraint the Interpreters URI by using some type level function

export function foo<
    E,
    A,
    ProgURI extends ProgramURI,
    InterpURI extends SelectInterpURI<E, A, { type: t.Type<A, E> }> // This will only accepts URIs of Interpreters defining a member `type` of type t.Type<A, E> (io-ts)
  >(
    S: MorphADT<E, A, Tag, ProgURI, InterpURI>
  ) {

    // Here S will completes all the available members of known Interpreters with the acceptable Interpreters URIs, so matching the constraint above
    ...
  }

Roadmap

  • Switch to Monorepo
  • Interpreter for persistency (TypeORM)
  • Implement Algebra for APIs

Disclaimer

THIS LIBRARY IS USED INTO TWO PROFESSIONAL PROJECTS IN DEVELPOMENT AT THE MOMENT

BUT BEWARE, THIS REPO IS A POC (WORK-IN-PROGRESS) THE API IS UNLIKELY TO CHANGE TRENDEMOUSLY BUT YOU MAY BE SAFER TO CONSIDER IT UNSTABLE AND USE AT YOUR OWN RISK

0.8.0-rc.2

4 years ago

0.8.0-rc.1

4 years ago

0.8.0-rc.0

4 years ago

0.7.0

4 years ago

0.7.0-RC10

4 years ago

0.7.0-RC9

4 years ago

0.7.0-RC8

4 years ago

0.7.0-RC7

4 years ago

0.7.0-RC6

4 years ago

0.7.0-RC5

4 years ago

0.7.0-RC4

4 years ago

0.7.0-RC3

4 years ago

0.7.0-RC2

4 years ago

0.7.0-RC1

4 years ago

0.7.0-RC

4 years ago

0.6.4

4 years ago

0.6.3

4 years ago

0.6.2

4 years ago

0.6.1

4 years ago

0.6.0

4 years ago

0.5.0

4 years ago

0.4.1

4 years ago

0.4.0

4 years ago

0.3.19

4 years ago

0.3.17

4 years ago

0.3.16

4 years ago

0.3.18

4 years ago

0.3.15

4 years ago

0.3.14

4 years ago

0.3.13

4 years ago

0.3.12

4 years ago

0.3.11

4 years ago

0.3.10

4 years ago

0.3.9

4 years ago

0.3.8

4 years ago

0.3.7

4 years ago

0.3.5

5 years ago

0.3.4

5 years ago

0.3.3

5 years ago

0.3.2

5 years ago

0.3.1

5 years ago

0.3.0

5 years ago

0.2.0

5 years ago

0.1.1

5 years ago

0.1.0

5 years ago