Skip to content

Latest commit

 

History

History
175 lines (129 loc) · 4.26 KB

README.md

File metadata and controls

175 lines (129 loc) · 4.26 KB

@fsubal/algebraic-enum

Poor man's ADT-like enum for TypeScript using template literal types.

Why?

Mimicking Algebraic Data Type (ADT) using tagged union type is a popular pattern, like it is used in redux action creators.

This package makes you write them easily. This is useful not only for redux actions, but also for anything like "enum having values".

How to use

npm install @fsubal/algebraic-enum
import algebraic, { nullary } from "@fsubal/algebraic-enum";

const ItemAction = algebraic("ItemAction", {
  loaded: nullary,
  selectedOne: (nextId: number) => ({ nextId }),
});

ItemAction.loaded(); // => { type: 'ItemAction/loaded', payload: {} }
ItemAction.selectedOne(1); // => { type: 'ItemAction/selectedOne', payload: { nextId: 1 } }

You can get the type of all possible values using Case<typeof ...>

import { Case, unreachable, unreachableSilent } from "@fsubal/algebraic-enum";

type KnownItemAction = Case<typeof ItemAction>; // { type: 'ItemAction/loaded', payload: {} } | { type: 'ItemAction/selectedOne', payload: { nextId: number } }

const reducer = (currentState: State, action: KnownItemAction) =>
  immer(currentState, (state) => {
    switch (action.type) {
      case "ItemAction/loaded": {
        state.loading = false;
        break;
      }

      case "ItemAction/selectedOne": {
        // This IS inferred from action.type !!!!
        const { nextId } = action.payload;
        state.nextId = nextId;
        break;
      }

      default: {
        // You CAN check the cases are exhaustive
        unreachable(action);

        // use `unreachableSilent` if you do not want to throw an error
        unreachableSilent(action);
      }
    }
  });

You can configure the delimiter using createAlgebraic. You will see the name of type is still perfectly inferred.

import { createAlgebraic } from "@fsubal/algebraic-enum";

const algebraic = createAlgebraic({ delimiter: "::" });

const ItemAction = algebraic("ItemAction", {
  loaded: nullary,
  selectedOne: (nextId: number) => ({ nextId }),
});

ItemAction.selectedOne(1); // => { type: 'ItemAction::selectedOne', payload: { nextId: 1 } }

type KnownItemAction = Case<typeof ItemAction>; // { type: 'ItemAction::loaded', payload: {} } | { type: 'ItemAction::selectedOne', payload: { nextId: number } }

Known limitations

This package cannot create enum with generic type (like Option<T> or Either<L, R>) in streight manner.

// YOU CANNOT DO THIS !!!
const Option = algebraic<T>("Option", {
  Some(value: T) {
    return value;
  },
  None: nullary,
});

// YOU CANNOT DO THIS TOO !!!
const Option = algebraic("Option", {
  Some<T>(value: T) {
    return value;
  },
  None: nullary,
});

// This will be `unknown` type...
Option.Some(1).payload;

You can workaround like this ( this is because it is "Poor man's ADT-like enum" ).

function Option<T = never>() {
  return algebraic("Option", {
    Some: (value: T) => value,
    None: nullary,
  });
}

Option<number>().Some(1);

But you cannot use Case<T> for Option<T>. You will find that Case<ReturnType<typeof Option>> is like...

| {
    type: "Option/Some";
    payload: unknown; // Cannot be inferred
  }
| {
    type: "Option/None";
    payload: {};
  }

This is rooted in TypeScript compiler's limitation ( you cannot use generic function for ReturnType. )

You can do this instead.

function Option<T = never>() {
  return algebraic("Option", {
    Some: (value: T) => value,
    None: nullary,
  });
}

const OptionNumber = Option<number>();

OptionNumber.Some(1);

type KnownOptionNumber = Case<typeof OptionNumber>;

Or if you would like to add richer behaviors, delegation/composition inside class is recommended. See our examples

class Option<T> {
  private options = algebraic('Option', {
    Some: (value: T) => value,
    None: nullary,
  })
  
  private value: Case<Option<T>['options']> = this.options.None()

  constructor(value: T | null | undefined) {
    if (value != null) {
      this.value = this.options.Some(value)
    }
  }
}

Development

WIP

See also