Skip to content

A small library aims to improve better tagged-unions/discriminated-unions supporting for TypeScript

License

Notifications You must be signed in to change notification settings

Lucifier129/coproduct

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

19 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

coproduct

npm version coproduct workflow Documentation Maintenance License: MIT Twitter: guyingjie129

A small library aims to improve better tagged-unions/discriminated-unions supporting for TypeScript

Benefits

  • Small bundled size(just 1kb)
  • Easy to use with just a few apis to learn
  • Improving Type-Safety for your TypeScript project via exhaustive pattern-matching

Installation

yarn add coproduct
npm install --save coproduct

Usage

For redux app

// state type
type CounterState = {
  count: number;
};

// action type
type CounterAction =
  | {
      type: 'incre';
    }
  | {
      type: 'decre';
    }
  | {
      type: 'increBy';
      step: number;
    }
  | {
      type: 'decreBy';
      step: number;
    };

// reducer with match
const counterReducer = (
  state: CounterState,
  action: CounterAction
): CounterState => {
  return match(action).case({
    incre: () => ({
      ...state,
      count: state.count + 1,
    }),
    decre: () => ({
      ...state,
      count: state.count - 1,
    }),
    increBy: ({ step }) => ({
      ...state,
      count: state.count + step,
    }),
    decreBy: ({ step }) => ({
      ...state,
      count: state.count - step,
    }),
  });
};

// reducer without match
const counterReducer = (
  state: CounterState,
  action: CounterAction
): CounterState => {
  if (action.type === 'incre') {
    return {
      ...state,
      count: state.count + 1,
    };
  } else if (action.type === 'decre') {
    return {
      ...state,
      count: state.count - 1,
    };
  } else if (action.type === 'increBy') {
    return {
      ...state,
      count: state.count + action.step,
    };
  } else if (action.type === 'decreBy') {
    return {
      ...state,
      count: state.count - action.step,
    };
  }

  throw new Error(`Unexpected action: ${action}`);
};

Basic usage

import { match } from 'coproduct';

export type Option<T> = {
  type: 'Some',
  value: T
} | {
  type: 'None'
}

export const None = {
  type: 'None' as const;
}
export const Some = <T>(value: T) => ({
  type: 'Some' as const,
  value,
});

const show = <T>(data: Option<T>) => {
  return match(data).case({
    Some: data => `some: ${data.value}`,
    None: () => 'none',
  });
};

const value0 = Some(1);
const value1 = None;

expect(show(value0)).toBe('some: 1');
expect(show(value1)).toBe('none');

// you can use if/else to match manually if you want
const show = <T>(data: Option<T>) => {
  if (data.type === 'Some') {
    return `some: ${data.some}`;
  } else if (data.type === 'None') {
    return 'none';
  }
  throw new Error(`Unexpected data: ${data}`);
};

You don't need to define your own option type, coproduct has built-in Option and Result.

import { match, Option, Some, None, Result, Ok, Err } from 'coproduct';

const show = <T>(data: Option<T>) => {
  return match(data).case({
    Some: data => `some: ${data.value}`,
    None: () => 'none',
  });
};

expect(show(Some(1))).toBe('some: 1');
expect(show(None)).toBe('none');

const showResult = <T>(result: Result<T>) => {
  return match(result).case({
    Ok: data => `ok: ${data.value}`,
    Err: error => `err: ${error.info}`,
  });
};

expect(showResult(Ok(1))).toBe('ok: 1');
expect(showResult(Err('error'))).toBe('err: error');

Api

match(data).case(patterns)

match(data).case(patterns) perform exhaustive pattern-matching for data, every case in data should has its own visitor function.

Note: you can use _: () => R as default handler for unmatched case.

createMatch(tagField) => match

You can create your own match function with tagField to match your data.

The default match of coproduct was created via createMatch('type')

const match = createMatch('tag');

type Data =
  | {
      tag: 'a';
      value: string;
    }
  | {
      tag: 'b';
      value: number;
    };

const handleData = (data: Data) => {
  return match(data).case({
    a: data => `a: ${data.value}`,
    b: data => `b: ${data.value}`,
  });
};

handleData({ tag: 'a', value: 'hello' }); // 'a: hello'
handleData({ tag: 'b', value: 1 }); // 'b: 1'

Some(value)

Some(value) return the value with the Some<T> case of Option Type.

None

None is the value with the None case of Option Type

Ok(value)

Ok(value) return the value with the Ok<T> case of Result Type.

Err(message)

Err(message) return the value with the Err<E> case of Result Type.

Caveats

  • The name $tag is reserved for $tag property of tagged object, it can't be used as a tag name.
  • The symbol _ can't be used as a tag name since it's a reserved filed in coproduct as placeholder for default case.

Contribution Guide

# test
npm run test

# build
npm run build

Author

πŸ‘€ Jade Gu

🀝 Contributing

Contributions, issues and feature requests are welcome!

Feel free to check issues page.

Show your support

Give a ⭐️ if this project helped you!

πŸ“ License

Copyright Β© 2022 Jade Gu.

This project is MIT licensed.

About

A small library aims to improve better tagged-unions/discriminated-unions supporting for TypeScript

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published