├── .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 | [![Library minified size](https://badgen.net/bundlephobia/min/@bitty/maybe)](https://bundlephobia.com/result?p=@bitty/maybe) 4 | [![Library minified + gzipped size](https://badgen.net/bundlephobia/minzip/@bitty/maybe)](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 | --------------------------------------------------------------------------------