New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

ts-union

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ts-union

ADT (sum type) in typescript inspired by ML language family

  • 2.0.0
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
1.1K
increased by0.56%
Maintainers
1
Weekly downloads
 
Created
Source

ts-union

Tiny library (<1Kb unminified & unzipped) for algebraic sum types in typescript. Inspired by unionize and F# discriminated-unions (and other ML languages)

Installation

npm add ts-union

NOTE: uses features from typescript 3.0 (such as unknown type)

Usage

Define

import { Union, of } from 'ts-union';

const PaymentMethod = Union({
  Cash: of<void>(), // or just of()
  Check: of<CheckNumber>(),
  CreditCard: of<CardType, CardNumber>()
});

type CheckNumber = number;
type CardType = 'MasterCard' | 'Visa';
type CardNumber = string;

Construct

const cash = PaymentMethod.Cash();
const check = PaymentMethod.Check(15566909);
const card = PaymentMethod.CreditCard('Visa', '1111-566-...');

// or destructure it to simplify construction :)
const { Cash, Check, CreditCard } = PaymentMethod;
const anotherCheck = Check(566541123);

match

const str = PaymentMethod.match(cash, {
  Cash: () => 'cash',
  Check: n => `check num: ${n.toString()}`,
  CreditCard: (type, n) => `${type} ${n}`
});

Also supports deferred (curried) matching and default case.

const toStr = PaymentMethod.match({
  Cash: () => 'cash',
  default: _v => 'not cash' // _v is the union obj
});

const str = toStr(card); // "not cash"

if (aka simplified match)

const str = PaymentMethod.if.Cash(cash, () => 'yep'); // "yep"
// typeof str === string | undefined

You can provide else case as well, then 'undefined' type will be removed from the result.

// typeof str === string
const str = PaymentMethod.if.Check(
  cash,
  n => `check num: ${n.toString()}`,
  _v => 'not check' // _v is the union obj that is passed in
); // str === 'not check'

Generic version

// Pass a function that accepts a type token and returns a record
const Maybe = Union(val => ({
  Nothing: of<void>(),
  Just: of(val) // Just has type Of<[Generic]>
}));

Note that val is a value of the special type Generic that will be substituted with an actual type later on. It is just a variable name, pls feel free to name it whatever you feel like :) Maybe a, T or TPayload?

This feature can be handy to model network requests (like in Redux):

const ReqResult = Union(data => ({
  Pending: of<void>(),
  Ok: of(data),
  Err: of<string | Error>()
}));

// res is inferred as UnionValG<string, ...>
const res = ReqResult.Ok('this is awesome!');

const status = ReqResult.match(res, {
  Pending: () => 'Thinking...',
  Err: err =>
    typeof err === 'string' ? `Oops ${err}` : `Exception ${err.message}`,
  Ok: str => `Ok, ${str}`
}); // 'Ok, this is awesome!'

Let's try to build map and bind functions for Maybe:

const { Nothing, Just } = Maybe;

// GenericValType is a helper that allows you to substitute Generic token type.
type MaybeVal<T> = GenericValType<T, typeof Maybe.T>;

const map = <A, B>(val: MaybeVal<A>, f: (a: A) => B) =>
  Maybe.match(val, {
    Just: v => Just(f(v)),
    Nothing: () => Nothing<B>() // note that we have to explicitly provide B type here
  });

const bind = <A, B>(val: MaybeVal<A>, f: (a: A) => MaybeVal<B>) =>
  Maybe.if.Just(val, a => f(a), n => (n as unknown) as MaybeVal<B>);

map(Just('a'), s => s.length); // -> Just(1)
bind(Just(100), n => Just(n.toString())); // -> Just('100')

map(Nothing<string>(), s => s.length); // -> Nothing

And if you want to extend Maybe with these functions:

const TempMaybe = Union(val => ({
  Nothing: of(),
  Just: of(val)
}));

const map = .....
const bind = .....

// TempMaybe is just an object, so this is perfectly legit
export const Maybe = {...TempMaybe, map, bind};

Type of resulted objects

Atm types of union values are opaque. That allows me to experiment with different underlying data structures.

type CashType = typeof cash;
// UnionVal<{Cash:..., Check:..., CreditCard:...}>
// and it is the same for card and check

The UnionVal<...> type for PaymentMethod is accessible via phantom property T

type PaymentMethodType = typeof PaymentMethod.T;
// UnionVal<{Cash:..., Check:..., CreditCard:...}>

Api and implementation details

If you will try to log a union value you will see just an array.

console.log(PaymentMethod.Check(15566909));
// ['Check', [15566909]]

All union values are arrays. The first element is the case key and the second is payload array. I decided not to expose that through typings but I might reconsider that in the future. You cannot use it for redux actions, however you can safely use it for redux state.

Api

Use Union to define shape

import { Union, of } from 'ts-union';

const U = Union({
  Simple: of(), // or of<void>(). no payload.
  One: of<string>(), // one argument
  Const: of(3), // one constant argument that is baked in
  Two: of<string, number>(), // two arguments
  Three: of<string, number, boolean>() // three
});

// generic version
const Option = Union(t => ({
  None: of<void>(),
  Some: of(t) // Note: t is a value of the special type Generic
}));

Let's take a closer look at of function

interface Types {
  <T = void>(): Of<[T]>;
  (g: Generic): Of<[Generic]>;
  <T>(val: T): Const<T>;
  <T1, T2>(): Of<[T1, T2]>;
  <T1, T2, T3>(): Of<[T1, T2, T3]>;
}
declare const of: Types;

the actual implementation is pretty simple:

export const of: Types = ((val: any) => val) as any;

We just capture the constant and don't really care about the rest. Typescript will guide us to provide proper number of args for each case.

match accepts either a full set of props or a subset with a default case.

// typedef for match function. Note there is a curried version
export type MatchFunc<Record> = {
  <Result>(cases: MatchCases<Record, Result>): (
    val: UnionVal<Record>
  ) => Result;
  <Result>(val: UnionVal<Record>, cases: MatchCases<Record, Result>): Result;
};

if either accepts a function that will be invoked (with a match) and/or else case.

// typedef for if case for one argument.
// Note it doesn't throw but can return undefined
{
    <R>(val: UnionVal<Rec>, f: (a: A) => R): R | undefined;
    <R>(val: UnionVal<Rec>, f: (a: A) => R, els: (v: UnionVal<Rec>) => R): R;
}

GenericValType is a type that helps with generic union values. It just replaces Generic token type with provided Type.

type GenericValType<Type, Val> = Val extends UnionValG<infer _Type, infer Rec>
  ? UnionValG<Type, Rec>
  : never;

// Example
import { Union, of, GenericValType } from 'ts-union';
const Maybe = Union(t => ({ Nothing: of(), Just: of(t) }));
type MaybeVal<T> = GenericValType<T, typeof Maybe.T>;

And that is the whole api.

Technically breaking changes going 1.2 -> 2.0

There should be no breaking changes but I removed some exported types to reduce api surface.

export {
  Union, // the main entry point function
  of, // helper to define cases payload
  GenericValType, // helper type for working with generic unions
  UnionObj, // Non generic union object: Constructors, match, if
  GenericUnionObj // generic version of it
};

Breaking changes going 1.1 -> 1.2

  • t function to define shapes is renamed to of.
  • There is a different underlying data structure. So if you persisted the values somewhere it wouldn't be compatible with the new version.

The actual change is pretty simple:

type OldShape = [string, ...payload[any]];
// Note: no nesting
const oldShape = ['CreditCard', 'Visa', '1111-566-...'];

type NewShape = [string, payload[any]];
// Note: captured payload is nested
const newShape = ['CreditCard', ['Visa', '1111-566-...']];

That allows to reduce allocations and it opens up future api extensibility. Such as:

// namespaces to avoid collisions.
const withNamespace = ['CreditCard', ['Visa', '1111-566-...'], 'MyNamespace'];

Keywords

FAQs

Package last updated on 19 Nov 2018

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc