practical-fp / union-types

A Typescript library for creating discriminating union types.
MIT License
70 stars 2 forks source link

Match multiple union types #2

Open janwirth opened 2 years ago

janwirth commented 2 years ago

twop/ts-union provides the experimental matchWith syntax: https://github.com/twop/ts-union#experimental-matchwith

const State = Union({
  Loading: of(null),
  Loaded: of<number>(),
  Err: of<string>(),
});

const Ev = Union({
  ErrorHappened: of<string>(),
  DataFetched: of<number>(),
});

const { Loaded, Err, Loading } = State;

const transition = (prev: typeof State.T, ev: typeof Ev.T) =>
  State.match(prev, {
    Loading: () =>
      Ev.match(ev, {
        ErrorHappened: (err) => Err(err),
        DataFetched: (data) => Loaded(data),
      }),

    Loaded: (loadedData) =>
      // just add to the current loaded value as an example
      Ev.if.DataFetched(
        ev,
        (data) => Loaded(loadedData + data),
        () => prev
      ),

    default: (s) => s,
  });

Are ther any plans to support this? Maybe the following syntax could work:

matchExhaustive(dataAorB, {
   a: matchWith(dataCorD, {
      // matchWith puts the args to the constructors in an array
      c: ([[aPayload, aPayload2nd], [cPayload]]) => cPayload && aPaylod2,
      d: () => false
   }
   b: (arg) => true // or wildcard
})

This is probably very tricky to type, isn't it?

felixschorer commented 2 years ago

The focus of this module is to reduce the boilerplate code when defining discriminating unions, i.e. type guards and constructor functions. I've included some form of pattern matching as a convenience. If you need more advanced pattern matching, I suggest you give ts-pattern a try.

import { impl, tag, Variant } from "@practical-fp/union-types"
import { __, match, select } from 'ts-pattern'

type State =
  | Variant<"Loading">
  | Variant<"Loaded", number>
  | Variant<"Err", string>

const { Loading, Loaded, Err } = impl<State>()

type Ev =
  | Variant<"ErrorHappened", string>
  | Variant<"DataFetched", number>

const { ErrorHappened, DataFetched } = impl<Ev>()

function transition(prev: State, ev: Ev) {
  return match([prev, ev] as const)
    .with([
        tag("Loading"), 
        tag("ErrorHappened", select()),
    ], err => Err(err))
    .with([
        tag("Loading"), 
        tag("DataFetched", select()),
    ], data => Loaded(data))
    .with([
        tag("Loaded", select("loadedData")), 
        tag("DataFetched", select("data")),
    ], ({ loadedData, data }) => Loaded(loadedData + data))
    .with(__, () => prev)
    .exhaustive()
}

If you don't want to repeat the tags all the time, that would also be possible.

function transition(prev: State, ev: Ev) {
  return match([prev, ev] as const)
    .with([
        Loading(), 
        tag(ErrorHappened.tag, select()),
    ], err => Err(err))
    .with([
        Loading(), 
        tag(DataFetched.tag, select()),
    ], data => Loaded(data))
    .with([
        tag(Loaded.tag, select("loadedData")), 
        tag(DataFetched.tag, select("data")),
    ], ({ loadedData, data }) => Loaded(loadedData + data))
    .with(__, () => prev)
    .exhaustive()
}

I guess one could also extend impl<>() to enable this syntax. Perhaps I could do that in version 2.

function transition(prev: State, ev: Ev) {
  return match([prev, ev] as const)
    .with([
        Loading(),
        ErrorHappened.select(),
    ], err => Err(err))
    .with([
        Loading(), 
        DataFetched.select(),
    ], data => Loaded(data))
    .with([
       Loaded.select("loadedData"), 
       DataFetched.select("data"),
    ], ({ loadedData, data }) => Loaded(loadedData + data))
    .with(__, () => prev)
    .exhaustive()
}
felixschorer commented 2 years ago

I've created the v2 branch and published version 2.0.0-alpha1 to evaluate if an integration with ts-pattern makes any sense.

anilanar commented 2 years ago

Hmm, what version of ts-pattern do you rely on?

import { AnonymousSelectPattern, NamedSelectPattern } from "ts-pattern/lib/types/Pattern", I don't see a NamedSelectPattern type in ts-pattern v3.

felixschorer commented 2 years ago

@anilanar 3.3.3 according to the lock file.

felixschorer commented 2 years ago

I am currently working on a rewrite of the library. It will support discriminating union types of all shapes and forms (excluding tuples). As a side effect of this, I had to come up with a new matching syntax. The new syntax will allow matching on tuples.

type Union = Variant<"Foo", string> | Variant<"Bar", number>

const { Foo, Bar } = impl<Union>()

const tuple: [Union, Union] = [Bar(42), Foo("Hello, World!")]

const t = match2(tuple)
    .with([Foo, Foo], ([foo, foo]) => /* ... */)
    .with([Foo, Bar], ([foo, bar]) => /* ... */)
    .with([Bar, null], ([bar, rest]) => /* ... */)
    .done()
felixschorer commented 2 years ago

I've published version 2.0.0-alpha2 to evaluate the improved matching. It doesn't rely on ts-pattern anymore.

It also adds implFactory to create implementations for discriminating union types of different shapes.