https://github.com/lucifier129/coproduct
A small library aims to improve better tagged-unions/discriminated-unions supporting for TypeScript
https://github.com/lucifier129/coproduct
Last synced: about 1 year ago
JSON representation
A small library aims to improve better tagged-unions/discriminated-unions supporting for TypeScript
- Host: GitHub
- URL: https://github.com/lucifier129/coproduct
- Owner: Lucifier129
- License: mit
- Created: 2022-01-19T09:35:19.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2022-02-16T09:40:26.000Z (over 4 years ago)
- Last Synced: 2025-04-08T14:52:30.870Z (about 1 year ago)
- Language: TypeScript
- Homepage:
- Size: 472 KB
- Stars: 29
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# coproduct
[](https://www.npmjs.com/package/coproduct)

[](https://github.com/Lucifier129/coproduct#readme)
[](https://github.com/Lucifier129/coproduct/graphs/commit-activity)
[](https://github.com/Lucifier129/coproduct/blob/master/LICENSE)
[](https://twitter.com/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
```shell
yarn add coproduct
npm install --save coproduct
```
## Usage
For redux app
```typescript
// 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
```typescript
import { match } from 'coproduct';
export type Option = {
type: 'Some',
value: T
} | {
type: 'None'
}
export const None = {
type: 'None' as const;
}
export const Some = (value: T) => ({
type: 'Some' as const,
value,
});
const show = (data: Option) => {
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 = (data: Option) => {
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`.
```typescript
import { match, Option, Some, None, Result, Ok, Err } from 'coproduct';
const show = (data: Option) => {
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 = (result: Result) => {
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')`
```ts
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` 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` case of `Result Type`.
### Err(message)
`Err(message)` return the value with the `Err` 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
```shell
# test
npm run test
# build
npm run build
```
## Author
👤 **Jade Gu**
- Twitter: [@guyingjie129](https://twitter.com/guyingjie129)
- Github: [@Lucifier129](https://github.com/Lucifier129)
## 🤝 Contributing
Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/Lucifier129/coproduct/issues).
## Show your support
Give a ⭐️ if this project helped you!
## 📝 License
Copyright © 2022 [Jade Gu](https://github.com/Lucifier129).
This project is [MIT](https://github.com/Lucifier129/coproduct/blob/master/LICENSE) licensed.