├── .gitignore
├── LICENSE
├── README.md
├── examples
└── getLanguage
│ └── getLanguage.ts
├── package.json
├── rollup.config.js
├── src
├── Maybe.ts
├── None.ts
├── Some.ts
├── fromFalsy.ts
├── fromNullish.ts
├── fromPredicate.ts
├── isMaybe.ts
├── main.ts
└── tryCatch.ts
├── tsconfig.build.json
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Unversioned files and folders.
2 | # References at https://git-scm.com/docs/gitignore.
3 |
4 | # Finder's configuration files (Mac).
5 | .DS_Store
6 |
7 | # Node.js modules.
8 | node_modules/
9 |
10 | # NPM's lockfiles.
11 | # We're using PNPM and it provides its own lockfile.
12 | npm-lockfile.json
13 | npm-shrinkwrap.json
14 |
15 | # Yarn's lockfile
16 | # We're using PNPM and it provides its own lockfile.
17 | yarn.lock
18 |
19 | # Log's files.
20 | *.log
21 | *.log.*
22 |
23 | # Generated bundles and type definitions.
24 | dist/
25 |
26 | # Generated type declarations.
27 | types/
28 |
29 | # Transpiled modules and their source-maps.
30 | src/**/*.js
31 | src/**/*.js.map
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019-current Vitor Luiz Cavalcanti
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `@bitty/maybe`
2 |
3 | [](https://bundlephobia.com/result?p=@bitty/maybe)
4 | [](https://bundlephobia.com/result?p=@bitty/maybe)
5 |
6 | ## Installation
7 |
8 | This library is published in the NPM registry and can be installed using any compatible package manager.
9 |
10 | ```sh
11 | npm install @bitty/maybe --save
12 |
13 | # For Yarn, use the command below.
14 | yarn add @bitty/maybe
15 | ```
16 |
17 | ### Installation from CDN
18 |
19 | This module has a UMD bundle available through JSDelivr and Unpkg CDNs.
20 |
21 | ```html
22 |
23 |
24 |
25 |
26 |
27 |
28 |
34 | ```
35 |
36 | ## Examples
37 |
38 | - [`getLanguage`](./examples/getLanguage/getLanguage.ts) is a function that resolves language in application. Its a pretty common case and it was implemented in the way I think we should use `Maybe`.
39 |
40 | ## License
41 |
42 | Released under [MIT License](./LICENSE).
43 |
--------------------------------------------------------------------------------
/examples/getLanguage/getLanguage.ts:
--------------------------------------------------------------------------------
1 | import * as Maybe from '../../';
2 | // The import statement in your application would be:
3 | // import * as Maybe from '@bitty/maybe';
4 |
5 | // ..:: Context ::..
6 |
7 | enum LanguageEnum {
8 | PORTUGUESE = 'pt-BR',
9 | ENGLISH = 'en-US',
10 | }
11 |
12 | type User = {
13 | id: string;
14 | language?: LanguageEnum;
15 | };
16 |
17 | type Organization = {
18 | id: string;
19 | language: LanguageEnum;
20 | };
21 |
22 | // An example of function that may return `null` or an empty language.
23 | const getCurrentUser = (): null | User => {
24 | if (Math.random() > .333)
25 | throw new Error('Couldn\'t get current organization.');
26 | if (Math.random() > .666)
27 | return {
28 | id: '...',
29 | language: undefined
30 | };
31 | return {
32 | id: '...',
33 | language: LanguageEnum.PORTUGUESE
34 | };
35 | };
36 |
37 |
38 | // An example of function that may throw an error.
39 | const getCurrentOrganization = (): Organization => {
40 | if (Math.random() > .5)
41 | throw new Error('Couldn\'t get current organization.');
42 | return {
43 | id: '...',
44 | language: LanguageEnum.ENGLISH
45 | };
46 | };
47 |
48 | // ..:: Business ::..
49 |
50 | // 1. Get user language.
51 | // 2. If can't, get organization language.
52 | // 3. If can't, get first browser language similar to suported languages.
53 | // 4. If can't, use ENGLISH language as fallback.
54 |
55 | // ..:: Implementation ::..
56 |
57 | const getLanguageFromUser = () =>
58 | Maybe.fromNullish(getCurrentUser())
59 | .chain((user) => Maybe.fromNullish(user.language));
60 |
61 | const getLanguageFromOrganization = () =>
62 | Maybe.tryCatch(getCurrentOrganization)
63 | .map((organization) => organization.language);
64 |
65 | const resolveLanguage = (language: string): Maybe.Maybe => {
66 | if (language.startsWith('pt')) return Maybe.Some(LanguageEnum.PORTUGUESE);
67 | if (language.startsWith('en')) return Maybe.Some(LanguageEnum.ENGLISH);
68 | return Maybe.None;
69 | };
70 |
71 | const getLanguageFromBrowser = () =>
72 | Maybe.Some(window.navigator.languages)
73 | .chain(([ language ]) => Maybe.fromNullish(language))
74 | .chain(resolveLanguage);
75 |
76 | const FALLBACK_LANGUAGE = LanguageEnum.ENGLISH;
77 |
78 | const getLanguage = (): LanguageEnum =>
79 | getLanguageFromUser()
80 | .alt(getLanguageFromOrganization)
81 | .alt(getLanguageFromBrowser)
82 | .getOrElse(() => FALLBACK_LANGUAGE);
83 |
84 | export default getLanguage;
85 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bitty/maybe",
3 | "version": "0.2.0",
4 | "description": "Maybe ",
5 | "cdn": "./dist/Maybe.umd.min.js",
6 | "main": "./dist/Maybe.js",
7 | "types": "./types/main.d.ts",
8 | "unpkg": "./dist/Maybe.umd.min.js",
9 | "module": "./dist/Maybe.esm.js",
10 | "jsdelivr": "./dist/Maybe.umd.min.js",
11 | "umd:main": "./dist/Maybe.umd.js",
12 | "exports": {
13 | ".": [
14 | {
15 | "import": "./dist/Maybe.mjs",
16 | "require": "./dist/Maybe.js",
17 | "default": "./dist/Maybe.js"
18 | },
19 | "./dist/Maybe.js"
20 | ]
21 | },
22 | "files": [
23 | "dist/",
24 | "types/"
25 | ],
26 | "scripts": {
27 | "build": "pnpm run build:transpile && pnpm run build:bundle",
28 | "build:transpile": "tsc --project ./tsconfig.build.json",
29 | "build:bundle": "rollup --config rollup.config.js",
30 | "prepublishOnly": "pnpm run build"
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "git+https://github.com/VitorLuizC/maybe.git"
35 | },
36 | "keywords": [
37 | "maybe",
38 | "maybe-type",
39 | "maybe-monad",
40 | "optional",
41 | "optional-type",
42 | "optional-typing",
43 | "functional-programming",
44 | "functional",
45 | "fp",
46 | "type-safety",
47 | "type-safe",
48 | "typescript",
49 | "bili",
50 | "ava"
51 | ],
52 | "author": {
53 | "url": "https://vitorluizc.github.io/",
54 | "name": "Vitor Luiz Cavalcanti",
55 | "email": "vitorluizc@outlook.com"
56 | },
57 | "license": "MIT",
58 | "bugs": {
59 | "url": "https://github.com/VitorLuizC/maybe/issues"
60 | },
61 | "homepage": "https://github.com/VitorLuizC/maybe#readme",
62 | "devDependencies": {
63 | "@rollup/plugin-node-resolve": "^13.1.3",
64 | "rollup": "^2.63.0",
65 | "rollup-plugin-terser": "^7.0.2",
66 | "typescript": "^4.5.4"
67 | },
68 | "dependencies": {
69 | "@bitty/falsy": "workspace:*",
70 | "@bitty/nullish": "workspace:*",
71 | "@bitty/predicate": "workspace:*"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { terser } from 'rollup-plugin-terser';
2 | import resolve from '@rollup/plugin-node-resolve';
3 |
4 | /**
5 | * Creates an output options object.
6 | * @param {import('rollup').OutputOptions} options
7 | * @returns {import('rollup').OutputOptions}
8 | */
9 | const Option = (options) => ({
10 | exports: 'named',
11 | sourcemap: true,
12 | ...options,
13 | });
14 |
15 | /**
16 | * An object with all configuration for `Rollup.js`.
17 | * @type {import('rollup').RollupOptions[]}
18 | */
19 | const options = [
20 | {
21 | input: './src/main.js',
22 | output: [
23 | Option({
24 | file: './dist/Maybe.js',
25 | format: 'commonjs',
26 | }),
27 | Option({
28 | file: './dist/Maybe.esm.js',
29 | format: 'esm',
30 | }),
31 | Option({
32 | file: './dist/Maybe.mjs',
33 | format: 'esm',
34 | }),
35 | ],
36 | },
37 | {
38 | input: './src/main.js',
39 | plugins: [resolve()],
40 | output: [
41 | Option({
42 | file: './dist/Maybe.umd.js',
43 | name: 'Maybe',
44 | format: 'umd',
45 | }),
46 | Option({
47 | file: './dist/Maybe.umd.min.js',
48 | name: 'Maybe',
49 | format: 'umd',
50 | plugins: [terser()],
51 | }),
52 | ],
53 | },
54 | ];
55 |
56 | export default options;
57 |
--------------------------------------------------------------------------------
/src/Maybe.ts:
--------------------------------------------------------------------------------
1 | import type None from './None.js';
2 | import type Some from './Some.js';
3 |
4 | type Maybe = None | Some;
5 |
6 | export type MaybePattern = {
7 | none: () => T2;
8 | some: (value: T) => T2;
9 | };
10 |
11 | export interface MaybeMethods {
12 | alt(fn: () => Maybe): Maybe;
13 |
14 | map(fn: (value: T) => T2): Maybe;
15 |
16 | then(fn: (value: T) => T2 | Maybe): Maybe;
17 |
18 | chain(fn: (value: T) => Maybe): Maybe;
19 |
20 | isNone(): this is None;
21 |
22 | isSome(): this is Some;
23 |
24 | match(pattern: MaybePattern): T2;
25 |
26 | fold(onLeft: () => T2, onRight: (value: T) => T2): T2;
27 |
28 | getOrElse(onLeft: () => T): T;
29 |
30 | unwrap(): T | null;
31 | }
32 |
33 | export default Maybe;
34 |
--------------------------------------------------------------------------------
/src/None.ts:
--------------------------------------------------------------------------------
1 | import type { MaybeMethods } from './Maybe.js';
2 |
3 | interface None extends MaybeMethods {
4 | _kind: 'None';
5 | }
6 |
7 | const None: None = {
8 | _kind: 'None',
9 | alt: (fn) => fn(),
10 | map: () => None,
11 | then: () => None,
12 | chain: () => None,
13 | isNone: () => true,
14 | isSome: () => false,
15 | match: ({ none }) => none(),
16 | fold: (onLeft) => onLeft(),
17 | getOrElse: (onLeft) => onLeft(),
18 | unwrap: () => null,
19 | };
20 |
21 | export default None;
22 |
--------------------------------------------------------------------------------
/src/Some.ts:
--------------------------------------------------------------------------------
1 | import type { MaybeMethods } from './Maybe';
2 | import isMaybe from './isMaybe';
3 |
4 | interface Some extends MaybeMethods {
5 | _kind: 'Some';
6 | }
7 |
8 | function Some(value: T): Some {
9 | return {
10 | _kind: 'Some',
11 | alt: () => Some(value),
12 | map: (fn) => Some(fn(value)),
13 | then: (fn) => {
14 | const valueOrMaybe = fn(value);
15 | return isMaybe(valueOrMaybe) ? valueOrMaybe : Some(valueOrMaybe);
16 | },
17 | chain: (fn) => fn(value),
18 | match: ({ some }) => some(value),
19 | fold: (_, onSome) => onSome(value),
20 | isNone: () => false,
21 | isSome: () => true,
22 | getOrElse: () => value,
23 | unwrap: () => value,
24 | };
25 | }
26 |
27 | export default Some;
28 |
--------------------------------------------------------------------------------
/src/fromFalsy.ts:
--------------------------------------------------------------------------------
1 | import type Maybe from './Maybe.js';
2 |
3 | import isFalsy, { Falsy, NonFalsy } from '@bitty/falsy';
4 |
5 | import None from './None.js';
6 | import Some from './Some.js';
7 |
8 | export default function fromFalsy(value?: Falsy | R): Maybe> {
9 | return isFalsy(value) ? None : Some(value as NonFalsy);
10 | }
11 |
--------------------------------------------------------------------------------
/src/fromNullish.ts:
--------------------------------------------------------------------------------
1 | import type Maybe from './Maybe.js';
2 |
3 | import isNullish, { Nullish, NonNullish } from '@bitty/nullish';
4 |
5 | import None from './None.js';
6 | import Some from './Some.js';
7 |
8 | export default function fromNullish(
9 | value?: Nullish | R,
10 | ): Maybe> {
11 | return isNullish(value) ? None : Some(value as NonNullish);
12 | }
13 |
--------------------------------------------------------------------------------
/src/fromPredicate.ts:
--------------------------------------------------------------------------------
1 | import type Maybe from './Maybe.js';
2 | import type { Predicate, Refinement } from '@bitty/predicate';
3 |
4 | import None from './None.js';
5 | import Some from './Some.js';
6 |
7 | export default function fromPredicate(
8 | predicate: Refinement,
9 | ): (value: T) => Maybe;
10 | export default function fromPredicate(
11 | predicate: Predicate,
12 | ): (value: T) => Maybe;
13 | export default function fromPredicate(predicate: Predicate) {
14 | return (value: unknown) => (predicate(value) ? Some(value) : None);
15 | }
16 |
--------------------------------------------------------------------------------
/src/isMaybe.ts:
--------------------------------------------------------------------------------
1 | import type Maybe from './Maybe';
2 | import None from './None';
3 |
4 | export default function isMaybe(value: unknown): value is Maybe {
5 | return (value as any)?._kind === 'Some' || value === None;
6 | }
7 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | export type { default as Maybe, MaybeMethods, MaybePattern } from './Maybe.js';
2 |
3 | export { default as None } from './None.js';
4 | export { default as Some } from './Some.js';
5 | export { default as fromFalsy } from './fromFalsy.js';
6 | export { default as fromNullish } from './fromNullish.js';
7 | export { default as fromPredicate } from './fromPredicate.js';
8 | export { default as tryCatch } from './tryCatch.js';
9 |
--------------------------------------------------------------------------------
/src/tryCatch.ts:
--------------------------------------------------------------------------------
1 | import type Maybe from './Maybe.js';
2 |
3 | import None from './None.js';
4 | import Some from './Some.js';
5 |
6 | export default function tryCatch(fn: () => T): Maybe {
7 | try {
8 | return Some(fn());
9 | } catch (error) {
10 | return None;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.build.json",
3 | "compilerOptions": {
4 | // Type declaration settings
5 | "declarationDir": "./types",
6 |
7 | // ECMAScript version settings
8 | "lib": ["ES5"]
9 | },
10 | "include": ["./src/**/*.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "ESNext"
5 | },
6 | "include": ["./src/**/*.ts"]
7 | }
8 |
--------------------------------------------------------------------------------