├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── funding.yml ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md ├── test.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | main: 7 | name: ${{matrix.node}} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v1 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/hydrogen 21 | - node 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.d.ts 3 | *.log 4 | coverage/ 5 | node_modules/ 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /funding.yml: -------------------------------------------------------------------------------- 1 | github: wooorm 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @callback Handler 3 | * Handle a value, with a certain ID field set to a certain value. 4 | * The ID field is passed to `zwitch`, and it’s value is this function’s 5 | * place on the `handlers` record. 6 | * @param {...any} parameters 7 | * Arbitrary parameters passed to the zwitch. 8 | * The first will be an object with a certain ID field set to a certain value. 9 | * @returns {any} 10 | * Anything! 11 | */ 12 | 13 | /** 14 | * @callback UnknownHandler 15 | * Handle values that do have a certain ID field, but it’s set to a value 16 | * that is not listed in the `handlers` record. 17 | * @param {unknown} value 18 | * An object with a certain ID field set to an unknown value. 19 | * @param {...any} rest 20 | * Arbitrary parameters passed to the zwitch. 21 | * @returns {any} 22 | * Anything! 23 | */ 24 | 25 | /** 26 | * @callback InvalidHandler 27 | * Handle values that do not have a certain ID field. 28 | * @param {unknown} value 29 | * Any unknown value. 30 | * @param {...any} rest 31 | * Arbitrary parameters passed to the zwitch. 32 | * @returns {void|null|undefined|never} 33 | * This should crash or return nothing. 34 | */ 35 | 36 | /** 37 | * @template {InvalidHandler} [Invalid=InvalidHandler] 38 | * @template {UnknownHandler} [Unknown=UnknownHandler] 39 | * @template {Record} [Handlers=Record] 40 | * @typedef Options 41 | * Configuration (required). 42 | * @property {Invalid} [invalid] 43 | * Handler to use for invalid values. 44 | * @property {Unknown} [unknown] 45 | * Handler to use for unknown values. 46 | * @property {Handlers} [handlers] 47 | * Handlers to use. 48 | */ 49 | 50 | const own = {}.hasOwnProperty 51 | 52 | /** 53 | * Handle values based on a field. 54 | * 55 | * @template {InvalidHandler} [Invalid=InvalidHandler] 56 | * @template {UnknownHandler} [Unknown=UnknownHandler] 57 | * @template {Record} [Handlers=Record] 58 | * @param {string} key 59 | * Field to switch on. 60 | * @param {Options} [options] 61 | * Configuration (required). 62 | * @returns {{unknown: Unknown, invalid: Invalid, handlers: Handlers, (...parameters: Parameters): ReturnType, (...parameters: Parameters): ReturnType}} 63 | */ 64 | export function zwitch(key, options) { 65 | const settings = options || {} 66 | 67 | /** 68 | * Handle one value. 69 | * 70 | * Based on the bound `key`, a respective handler will be called. 71 | * If `value` is not an object, or doesn’t have a `key` property, the special 72 | * “invalid” handler will be called. 73 | * If `value` has an unknown `key`, the special “unknown” handler will be 74 | * called. 75 | * 76 | * All arguments, and the context object, are passed through to the handler, 77 | * and it’s result is returned. 78 | * 79 | * @this {unknown} 80 | * Any context object. 81 | * @param {unknown} [value] 82 | * Any value. 83 | * @param {...unknown} parameters 84 | * Arbitrary parameters passed to the zwitch. 85 | * @property {Handler} invalid 86 | * Handle for values that do not have a certain ID field. 87 | * @property {Handler} unknown 88 | * Handle values that do have a certain ID field, but it’s set to a value 89 | * that is not listed in the `handlers` record. 90 | * @property {Handlers} handlers 91 | * Record of handlers. 92 | * @returns {unknown} 93 | * Anything. 94 | */ 95 | function one(value, ...parameters) { 96 | /** @type {Handler|undefined} */ 97 | let fn = one.invalid 98 | const handlers = one.handlers 99 | 100 | if (value && own.call(value, key)) { 101 | // @ts-expect-error Indexable. 102 | const id = String(value[key]) 103 | // @ts-expect-error Indexable. 104 | fn = own.call(handlers, id) ? handlers[id] : one.unknown 105 | } 106 | 107 | if (fn) { 108 | return fn.call(this, value, ...parameters) 109 | } 110 | } 111 | 112 | one.handlers = settings.handlers || {} 113 | one.invalid = settings.invalid 114 | one.unknown = settings.unknown 115 | 116 | // @ts-expect-error: matches! 117 | return one 118 | } 119 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType, expectAssignable} from 'tsd' 2 | import {zwitch} from './index.js' 3 | 4 | type Alpha = Record & {type: 'alpha'} 5 | type Bravo = Record & {type: 'bravo'} 6 | 7 | const handle = zwitch('type', { 8 | invalid(value) { 9 | expectType(value) 10 | throw new Error('!') 11 | }, 12 | unknown(value) { 13 | expectType(value) 14 | return 1 15 | }, 16 | handlers: { 17 | alpha(value: Alpha): 'a' { 18 | return 'a' 19 | }, 20 | bravo(value: Bravo): 'b' { 21 | expectType<'bravo'>(value.type) 22 | return 'b' 23 | } 24 | } 25 | }) 26 | 27 | // Unfortunately I can’t figure out a way to narrow this down :'( 28 | // PRs welcome! 29 | expectType<'a' | 'b'>(handle({type: 'alpha'})) 30 | expectType<'a' | 'b'>(handle({type: 'bravo'})) 31 | 32 | // This is the unknown case. 33 | expectType(handle({type: 'charlie'})) 34 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zwitch", 3 | "version": "2.0.4", 4 | "description": "Handle values based on a property", 5 | "license": "MIT", 6 | "keywords": [ 7 | "handle", 8 | "switch", 9 | "property" 10 | ], 11 | "repository": "wooorm/zwitch", 12 | "bugs": "https://github.com/wooorm/zwitch/issues", 13 | "funding": { 14 | "type": "github", 15 | "url": "https://github.com/sponsors/wooorm" 16 | }, 17 | "author": "Titus Wormer (https://wooorm.com)", 18 | "contributors": [ 19 | "Titus Wormer (https://wooorm.com)" 20 | ], 21 | "sideEffects": false, 22 | "type": "module", 23 | "main": "index.js", 24 | "types": "index.d.ts", 25 | "files": [ 26 | "index.d.ts", 27 | "index.js" 28 | ], 29 | "devDependencies": { 30 | "@types/node": "^18.0.0", 31 | "c8": "^7.0.0", 32 | "prettier": "^2.0.0", 33 | "remark-cli": "^11.0.0", 34 | "remark-preset-wooorm": "^9.0.0", 35 | "tsd": "^0.24.0", 36 | "type-coverage": "^2.0.0", 37 | "typescript": "^4.0.0", 38 | "xo": "^0.52.0" 39 | }, 40 | "scripts": { 41 | "prepack": "npm run build && npm run format", 42 | "build": "tsc --build --clean && tsc --build && tsd && type-coverage", 43 | "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", 44 | "test-api": "node --conditions development test.js", 45 | "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", 46 | "test": "npm run build && npm run format && npm run test-coverage" 47 | }, 48 | "prettier": { 49 | "tabWidth": 2, 50 | "useTabs": false, 51 | "singleQuote": true, 52 | "bracketSpacing": false, 53 | "semi": false, 54 | "trailingComma": "none" 55 | }, 56 | "xo": { 57 | "prettier": true 58 | }, 59 | "remarkConfig": { 60 | "plugins": [ 61 | "preset-wooorm" 62 | ] 63 | }, 64 | "typeCoverage": { 65 | "atLeast": 100, 66 | "detail": true, 67 | "strict": true, 68 | "ignoreFiles": [ 69 | "index.d.ts" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # zwitch 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![Downloads][downloads-badge]][downloads] 6 | [![Size][size-badge]][size] 7 | 8 | Handle values based on a field. 9 | 10 | ## Contents 11 | 12 | * [What is this?](#what-is-this) 13 | * [When should I use this?](#when-should-i-use-this) 14 | * [Install](#install) 15 | * [Use](#use) 16 | * [API](#api) 17 | * [`zwitch(key[, options])`](#zwitchkey-options) 18 | * [`one(value[, rest…])`](#onevalue-rest) 19 | * [`function handler(value[, rest…])`](#function-handlervalue-rest) 20 | * [Types](#types) 21 | * [Compatibility](#compatibility) 22 | * [Related](#related) 23 | * [Contribute](#contribute) 24 | * [Security](#security) 25 | * [License](#license) 26 | 27 | ## What is this? 28 | 29 | This is a tiny package that lets you `switch` between some field on objects. 30 | 31 | ## When should I use this? 32 | 33 | This package is very useful when mapping one AST to another. 34 | It’s a lot like a `switch` statement on one field, but it’s extensible. 35 | 36 | ## Install 37 | 38 | This package is [ESM only][esm]. 39 | In Node.js (version 14.14+, 16.0+), install with [npm][]: 40 | 41 | ```sh 42 | npm install zwitch 43 | ``` 44 | 45 | In Deno with [`esm.sh`][esmsh]: 46 | 47 | ```js 48 | import {zwitch} from 'https://esm.sh/zwitch@2' 49 | ``` 50 | 51 | In browsers with [`esm.sh`][esmsh]: 52 | 53 | ```html 54 | 57 | ``` 58 | 59 | ## Use 60 | 61 | ```js 62 | import {zwitch} from 'zwitch' 63 | 64 | const handle = zwitch('type', {invalid, unknown, handlers: {alpha: handleAlpha}}) 65 | 66 | handle({type: 'alpha'}) 67 | 68 | function handleAlpha() { /* … */ } 69 | ``` 70 | 71 | Or, with a `switch` statement: 72 | 73 | ```js 74 | const field = 'type' 75 | 76 | function handle(value) { 77 | let fn = invalid 78 | 79 | if (value && typeof value === 'object' && field in value) { 80 | switch (value[field]) { 81 | case 'alpha': 82 | fn = handleAlpha 83 | break 84 | default: 85 | fn = unknown 86 | break 87 | } 88 | } 89 | 90 | return fn.apply(this, arguments) 91 | } 92 | 93 | handle({type: 'alpha'}) 94 | 95 | function handleAlpha() { /* … */ } 96 | function unknown() { /* … */ } 97 | function invalid() { /* … */ } 98 | ``` 99 | 100 | ## API 101 | 102 | This package exports the identifier `zwitch`. 103 | There is no default export. 104 | 105 | ### `zwitch(key[, options])` 106 | 107 | Create a switch, based on a `key` (`string`). 108 | 109 | ##### `options` 110 | 111 | Options can be omitted and added later to `one`. 112 | 113 | ###### `options.handlers` 114 | 115 | Handlers to use, stored on `one.handlers` (`Record`, 116 | optional). 117 | 118 | ###### `options.unknown` 119 | 120 | Handler to use for unknown values, stored on `one.unknown` (`Function`, 121 | optional). 122 | 123 | ###### `options.invalid` 124 | 125 | Handler to use for invalid values, stored on `one.invalid` (`Function`, 126 | optional). 127 | 128 | ###### Returns 129 | 130 | See [`one`][one] (`Function`). 131 | 132 | ### `one(value[, rest…])` 133 | 134 | Handle one value. 135 | Based on the bound `key`, a respective handler will be called. 136 | If `value` is not an object, or doesn’t have a `key` property, the special 137 | “invalid” handler will be called. 138 | If `value` has an unknown `key`, the special “unknown” handler will be called. 139 | 140 | All arguments, and the context object (`this`), are passed through to the 141 | [handler][], and it’s result is returned. 142 | 143 | ###### `one.handlers` 144 | 145 | Map of [handler][]s (`Record`). 146 | 147 | ###### `one.invalid` 148 | 149 | Special [`handler`][handler] called if a value doesn’t have a `key` property. 150 | If not set, `undefined` is returned for invalid values. 151 | 152 | ###### `one.unknown` 153 | 154 | Special [`handler`][handler] called if a value does not have a matching 155 | handler. 156 | If not set, `undefined` is returned for unknown values. 157 | 158 | ### `function handler(value[, rest…])` 159 | 160 | Handle one value. 161 | 162 | ## Types 163 | 164 | This package is fully typed with [TypeScript][]. 165 | It exports the types `Handler`, `UnknownHandler`, `InvalidHandler`, and 166 | `Options`. 167 | 168 | ## Compatibility 169 | 170 | This package is at least compatible with all maintained versions of Node.js. 171 | As of now, that is Node.js 14.14+ and 16.0+. 172 | It also works in Deno and modern browsers. 173 | 174 | ## Related 175 | 176 | * [`mapz`](https://github.com/wooorm/mapz) 177 | — functional map 178 | 179 | ## Contribute 180 | 181 | Yes please! 182 | See [How to Contribute to Open Source][contribute]. 183 | 184 | ## Security 185 | 186 | This package is safe. 187 | 188 | ## License 189 | 190 | [MIT][license] © [Titus Wormer][author] 191 | 192 | 193 | 194 | [build-badge]: https://github.com/wooorm/zwitch/workflows/main/badge.svg 195 | 196 | [build]: https://github.com/wooorm/zwitch/actions 197 | 198 | [coverage-badge]: https://img.shields.io/codecov/c/github/wooorm/zwitch.svg 199 | 200 | [coverage]: https://codecov.io/github/wooorm/zwitch 201 | 202 | [downloads-badge]: https://img.shields.io/npm/dm/zwitch.svg 203 | 204 | [downloads]: https://www.npmjs.com/package/zwitch 205 | 206 | [size-badge]: https://img.shields.io/bundlephobia/minzip/zwitch.svg 207 | 208 | [size]: https://bundlephobia.com/result?p=zwitch 209 | 210 | [npm]: https://docs.npmjs.com/cli/install 211 | 212 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 213 | 214 | [esmsh]: https://esm.sh 215 | 216 | [typescript]: https://www.typescriptlang.org 217 | 218 | [contribute]: https://opensource.guide/how-to-contribute/ 219 | 220 | [license]: license 221 | 222 | [author]: https://wooorm.com 223 | 224 | [one]: #onevalue-rest 225 | 226 | [handler]: #function-handlervalue-rest 227 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import test from 'node:test' 3 | import {zwitch} from './index.js' 4 | 5 | test('zwitch(options)', function () { 6 | const handleNone = zwitch('type') 7 | 8 | assert.equal( 9 | handleNone(null), 10 | undefined, 11 | 'should not fail when not given an object' 12 | ) 13 | assert.equal(handleNone({}), undefined, 'should not fail without `key`') 14 | assert.equal( 15 | handleNone({type: 'unknown'}), 16 | undefined, 17 | 'should not fail with unknown `key`' 18 | ) 19 | 20 | const handleInvalid = zwitch('type', {invalid}) 21 | 22 | assert.throws( 23 | function () { 24 | handleInvalid(null) 25 | }, 26 | /Invalid: `null`/, 27 | 'should call `invalid` when not given an object' 28 | ) 29 | 30 | assert.throws( 31 | function () { 32 | handleInvalid({}) 33 | }, 34 | /Invalid: `\[object Object]`/, 35 | 'should call `invalid` when without key' 36 | ) 37 | 38 | const handleInvalidAndUndefined = zwitch('type', {invalid, unknown}) 39 | 40 | assert.throws( 41 | function () { 42 | handleInvalidAndUndefined({type: 'alpha'}) 43 | }, 44 | /Unknown: `alpha`/, 45 | 'should call `unknown` when unknown' 46 | ) 47 | 48 | const handleAll = zwitch('type', {unknown, invalid, handlers: {alpha, beta}}) 49 | 50 | assert.equal( 51 | handleAll({type: 'alpha', value: 'a'}), 52 | 'a', 53 | 'should call a handler' 54 | ) 55 | }) 56 | 57 | /** 58 | * @param {unknown} value 59 | * @returns {never} 60 | */ 61 | function invalid(value) { 62 | throw new Error('Invalid: `' + value + '`') 63 | } 64 | 65 | /** 66 | * @param {unknown} value 67 | * @returns {never} 68 | */ 69 | function unknown(value) { 70 | // @ts-expect-error: JS guarantees there’s a `type`, TS can’t do that. 71 | throw new Error('Unknown: `' + value.type + '`') 72 | } 73 | 74 | /** 75 | * @param {{type: string, value: string}} value 76 | */ 77 | function alpha(value) { 78 | return value.value 79 | } 80 | 81 | /** 82 | * @param {{type: string, value: number}} value 83 | * @return {number} 84 | */ 85 | function beta(value) { 86 | return value.value 87 | } 88 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/**.js"], 3 | "exclude": ["coverage", "node_modules"], 4 | "compilerOptions": { 5 | "checkJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "lib": ["es2020"], 11 | "module": "node16", 12 | "newLine": "lf", 13 | "skipLibCheck": true, 14 | "strict": true, 15 | "target": "es2020" 16 | } 17 | } 18 | --------------------------------------------------------------------------------