├── .editorconfig ├── .gitignore ├── .tool-versions ├── .yarn └── versions │ └── 95d7d7c4.yml ├── .yarnrc.yml ├── Changelog.md ├── Makefile ├── README.md ├── dprint.json ├── example ├── index.js ├── package.json └── yarn.lock ├── jest.config.js ├── package.json ├── perf ├── match.ts ├── package.json ├── tsconfig.json └── yarn.lock ├── src ├── const.ts ├── index.test.ts ├── index.ts ├── tsconfig-release-cjs.json ├── tsconfig-release-esm.json ├── tsconfig.json ├── types.d.ts └── types.nt ├── test ├── cases │ └── conformance │ │ ├── expand-capture.ts │ │ ├── match.ts │ │ ├── overlapping.ts │ │ ├── test-helper.ts │ │ ├── tsconfig.json │ │ ├── unify-all.ts │ │ ├── variable-capture.ts │ │ └── when.ts └── pack │ ├── commonjs │ ├── index.js │ └── package.json │ ├── esm │ ├── index.js │ └── package.json │ └── ts │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── tsconfig-base.json ├── tsconfig-release.json ├── tsconfig.json ├── tsconfig.node10.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | trim_trailing_whitespace = true 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | tsconfig.tsbuildinfo 4 | build 5 | dist 6 | *.tgz 7 | 8 | !.yarn/ 9 | .yarn/* 10 | !.yarn/versions/ 11 | perf/.yarn/ 12 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 16.4.2 2 | -------------------------------------------------------------------------------- /.yarn/versions/95d7d7c4.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dkendal/match-ts/723236011421e15ce1f83674e7ca86a466aa5869/.yarn/versions/95d7d7c4.yml -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-version.cjs 5 | spec: "@yarnpkg/plugin-version" 6 | 7 | yarnPath: .yarn/releases/yarn-3.1.0.cjs 8 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | This is the initial relase of pattern-match.js! 4 | 5 | - Full typing of the when/2 DSL 6 | - Pattern matching runtime component 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test test-release perf clean build build-release 2 | 3 | TSC_OPTS = --assumeChangesOnlyAffectDirectDependencies --verbose --listEmittedFiles 4 | 5 | log = @echo $$(tput smso)$(1)$$(tput rmso) 6 | 7 | newtype_source_files = $(shell find src -name '*.nt') 8 | newtype_to_typescript = $(shell find src -name '*.nt') 9 | 10 | all: build 11 | 12 | clean: 13 | yarn run tsc --build --clean 14 | rm -rf dist 15 | 16 | newtype: 17 | @# FIXME make this repeatable outside of my local computer 18 | racket -l dts/main src/types.nt | prettier --parser typescript > src/types.d.ts 19 | 20 | build: newtype 21 | yarn tsc --build $(TSC_OPTS) 22 | 23 | prepack: build-release test-release 24 | 25 | build-release: 26 | $(call log,# build-release / tsc ...) 27 | yarn run tsc --build tsconfig-release.json ${TSC_OPTS} 28 | $(call log,# build-release ✅) 29 | rm dist/*/*.tsbuildinfo 30 | 31 | test-release: \ 32 | build-release \ 33 | test-release-package-check \ 34 | test-release-cjs \ 35 | test-release-ts 36 | $(call log,# test-release ✅) 37 | 38 | test-release-package-check: 39 | $(call log,# test-release / Package exports ...) 40 | package-check 41 | 42 | test-release-cjs: 43 | $(call log,# test-release / CommonJS ...) 44 | yarn workspace test-pack-commonjs install 45 | yarn workspace test-pack-commonjs test 46 | $(call log,# test-release / CommonJS ✅) 47 | 48 | # FIXME: node esm support is weird 49 | test-release-esm: 50 | $(call log,# test-release / ESM) 51 | yarn workspace test-pack-esm install 52 | yarn workspace test-pack-esm test 53 | $(call log,# test-release / ESM ✅) 54 | 55 | test-release-ts: 56 | $(call log,# test-release / Typescript ...) 57 | yarn workspace test-pack-ts install 58 | yarn workspace test-pack-ts build 59 | yarn workspace test-pack-ts test 60 | $(call log,# test-release / Typescript ✅) 61 | 62 | perf: build 63 | for file in build/perf/*.js; do node $$file; done 64 | 65 | test: 66 | yarn tsc --project tsconfig.json 67 | yarn jest 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @dkendal/match 2 | 3 | Provides pattern matching features typically found in functional 4 | languages like Elixir/Erlang, ML, F\#, etc. 5 | 6 | ## Installation 7 | 8 | ```sh 9 | yarn add @dkendal/match 10 | ``` 11 | 12 | ## The `match` function 13 | 14 | The match function lets us compare a value against many patterns until 15 | we find one that matches. 16 | 17 | ```typescript 18 | import { __, match, when } from '@dkendal/match' 19 | 20 | match( 21 | [1, 2, 3], 22 | when([4, 5, 6], () => "This clause won't match"), 23 | when([1, __, 3], () => 'This clause will match, __ will match any value'), 24 | when(__, () => 'This clause would match any value'), 25 | ) 26 | 27 | 'This clause will match, __ will match any value' 28 | ``` 29 | 30 | The value that was passed into `match` is also passed to the callback: 31 | 32 | ```typescript 33 | const x = 10 34 | 35 | match( 36 | x, 37 | when(__, (value) => value * 10), 38 | ) 39 | 100 40 | ``` 41 | 42 | If none of the cases match and error is thrown: 43 | 44 | ```typescript 45 | match([1, 2, 3] 46 | when([1, 2], () => "this won't match as the array lengths don't agree")) 47 | // (Error) unmatched case: [1, 2, 3] 48 | ``` 49 | 50 | ## The `when` function 51 | 52 | The `when` function exists purely for typing. When you use it gives the callback 53 | the proper type expressed in the left-hand-side pattern: 54 | 55 | ```typescript 56 | when( 57 | V('checkout', { 58 | type: 'checkout', 59 | lineItems: [V('firstLineItem', { type: 'line_item', data: __ }), __.rest], 60 | }), 61 | ({ checkout, firstLineItem }) => { 62 | var checkout: { 63 | type: 'checkout' 64 | id: number 65 | lineItems: [{ 66 | type: 'line_item' 67 | id: number 68 | data: any 69 | }, any[]] 70 | } 71 | 72 | var firstLineItem: { 73 | type: 'line_item' 74 | id: number 75 | data: any 76 | } 77 | }, 78 | ) 79 | ``` 80 | 81 | ## `match` function continued 82 | 83 | Array's are matched as tuples, the lengths have to be the same: 84 | 85 | ```typescript 86 | match([1, 2, 3] 87 | when([1, 2], () => "this won't match"), 88 | when([1, 2, 3], () => "this will match")) 89 | "this will match" 90 | ``` 91 | 92 | The tail of a list can be matched with `__.rest`, or it's aliases 93 | `__.tail` or `__.tl`: 94 | 95 | ```typescript 96 | match( 97 | range(50, 100), 98 | when([50, 51, __.rest], () => 'this will match'), 99 | ) 100 | ;('this will match') 101 | ``` 102 | 103 | Primitive types can be matched with special operators: 104 | 105 | ```typescript 106 | const thingy: { 107 | data: { 108 | someNumber: 1 109 | metadata: { foobar: 'foobar' } 110 | someOtherProp: {} 111 | } 112 | list: [1, 2, 3] 113 | } 114 | 115 | match( 116 | thingy, 117 | when( 118 | { 119 | data: { someNumber: __.number, metadata: __.object }, 120 | list: [__, __.rest], 121 | }, 122 | () => 'this will match', 123 | ), 124 | ) 125 | ;('this will match') 126 | ``` 127 | 128 | You can capture variables and use them on the right hand side of match, if you 129 | use `when` the callback will be typed with all variable bindings: 130 | 131 | ```typescript 132 | const thingy: { 133 | meta: { 134 | type: 'contrived example', 135 | x: 1, 136 | y: 2, 137 | }, 138 | data: { 139 | a: { 140 | b: { 141 | c: { 142 | d: { 143 | message: 'hello world' 144 | value: 42 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } 151 | 152 | match(thingy, 153 | when({ meta: V('x'), 154 | data: { a: { b: { c: { d: { value: V('y') } } } } } 155 | }, (captures) => captures)) 156 | { 157 | x: { 158 | type: 'contrived example', 159 | x: 1, 160 | x: 2, 161 | } 162 | y: 42 163 | } 164 | ``` 165 | 166 | Variable captures can also apply a pattern that must also match for the 167 | case. 168 | 169 | ```typescript 170 | import { match, __, V } from "pattern-matching" 171 | 172 | const checkout = { 173 | object: "list", 174 | data: [ 175 | { 176 | id: "li_1", 177 | object: "item", 178 | ... 179 | }, 180 | {...}, 181 | {...} 182 | ] 183 | } 184 | match(value, 185 | when( { object: 'list', 186 | data: V('data')([ 187 | V('first')({ object: 'item' }), 188 | __.rest 189 | ]) 190 | }, (captures) => captures )) 191 | 192 | { 193 | data: [ 194 | { 195 | id: "li_1", 196 | object: "item", 197 | ... 198 | }, 199 | {...}, 200 | {...} 201 | ] 202 | first: { 203 | id: "li_1", 204 | object: "item", 205 | ... 206 | }, 207 | } 208 | ``` 209 | 210 | ## TODOs 211 | 212 | - [ \] Proper type inference right-hand-side of match case 213 | - [ \] Exhaustive type check for match cases 214 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "incremental": true, 3 | "lineWidth": 80, 4 | "indentWidth": 2, 5 | "typescript": { 6 | "semiColons": "asi", 7 | "quoteStyle": "preferSingle", 8 | "useBraces": "preferNone", 9 | "memberExpression.linePerExpression": true, 10 | "binaryExpression.linePerExpression": true 11 | }, 12 | "json": {}, 13 | "markdown": {}, 14 | "toml": {}, 15 | "includes": [ 16 | "**/*.{ts,tsx,js,jsx,cjs,mjs,json,md,toml}" 17 | ], 18 | "excludes": [ 19 | "**/node_modules", 20 | "**/*-lock.json" 21 | ], 22 | "plugins": [ 23 | "https://plugins.dprint.dev/typescript-0.55.0.wasm", 24 | "https://plugins.dprint.dev/json-0.13.0.wasm", 25 | "https://plugins.dprint.dev/markdown-0.10.0.wasm", 26 | "https://plugins.dprint.dev/toml-0.5.1.wasm" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { __, match, V, when } from '@dkendal/match' 2 | 3 | match( 4 | 1, 5 | when(__, () => { 6 | console.log('hello') 7 | }), 8 | ) 9 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@dkendal/match": "^0.1.0", 8 | "ts-node": "^10.2.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@cspotcode/source-map-consumer@0.8.0": 6 | version "0.8.0" 7 | resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" 8 | integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== 9 | 10 | "@cspotcode/source-map-support@0.6.1": 11 | version "0.6.1" 12 | resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz#118511f316e2e87ee4294761868e254d3da47960" 13 | integrity sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg== 14 | dependencies: 15 | "@cspotcode/source-map-consumer" "0.8.0" 16 | 17 | "@dkendal/match@^0.1.0": 18 | version "0.1.0" 19 | resolved "https://registry.yarnpkg.com/@dkendal/match/-/match-0.1.0.tgz#a93b9353980c40b2e4150d92c4562f25b398f216" 20 | integrity sha512-cIcnwwg+bEuWthsOUHHM+xC19bJgjxqtdO4+4tvpuzKeY18uYYNytC229pgyun1219KjdSK2PfQyvB8UgnljgA== 21 | dependencies: 22 | ts-toolbelt "^9.6.0" 23 | 24 | "@tsconfig/node10@^1.0.7": 25 | version "1.0.8" 26 | resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" 27 | integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== 28 | 29 | "@tsconfig/node12@^1.0.7": 30 | version "1.0.9" 31 | resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" 32 | integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== 33 | 34 | "@tsconfig/node14@^1.0.0": 35 | version "1.0.1" 36 | resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" 37 | integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== 38 | 39 | "@tsconfig/node16@^1.0.2": 40 | version "1.0.2" 41 | resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" 42 | integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== 43 | 44 | acorn-walk@^8.1.1: 45 | version "8.2.0" 46 | resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" 47 | integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== 48 | 49 | acorn@^8.4.1: 50 | version "8.5.0" 51 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" 52 | integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== 53 | 54 | arg@^4.1.0: 55 | version "4.1.3" 56 | resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" 57 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 58 | 59 | create-require@^1.1.0: 60 | version "1.1.1" 61 | resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" 62 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 63 | 64 | diff@^4.0.1: 65 | version "4.0.2" 66 | resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" 67 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 68 | 69 | make-error@^1.1.1: 70 | version "1.3.6" 71 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" 72 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 73 | 74 | ts-node@^10.2.1: 75 | version "10.2.1" 76 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.2.1.tgz#4cc93bea0a7aba2179497e65bb08ddfc198b3ab5" 77 | integrity sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw== 78 | dependencies: 79 | "@cspotcode/source-map-support" "0.6.1" 80 | "@tsconfig/node10" "^1.0.7" 81 | "@tsconfig/node12" "^1.0.7" 82 | "@tsconfig/node14" "^1.0.0" 83 | "@tsconfig/node16" "^1.0.2" 84 | acorn "^8.4.1" 85 | acorn-walk "^8.1.1" 86 | arg "^4.1.0" 87 | create-require "^1.1.0" 88 | diff "^4.0.1" 89 | make-error "^1.1.1" 90 | yn "3.1.1" 91 | 92 | ts-toolbelt@^9.6.0: 93 | version "9.6.0" 94 | resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz#50a25426cfed500d4a09bd1b3afb6f28879edfd5" 95 | integrity sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w== 96 | 97 | yn@3.1.1: 98 | version "3.1.1" 99 | resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" 100 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 101 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("@jest/types").Config.InitialOptions} */ 2 | 3 | const config = { 4 | transform: { 5 | '^.+\\.(ts|js)$': [ 6 | 'esbuild-jest', 7 | { 8 | sourcemap: 'true', 9 | target: 'node10', 10 | platform: 'node', 11 | }, 12 | ], 13 | }, 14 | testEnvironment: 'node', 15 | moduleFileExtensions: ['ts', 'js', 'json'], 16 | modulePathIgnorePatterns: ['/dist', '/lib'], 17 | } 18 | 19 | module.exports = config 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dkendal/match", 3 | "version": "0.3.0", 4 | "author": "Dylan Kendal ", 5 | "license": "MIT", 6 | "homepage": "https://github.com/dkendal/pattern-match.js", 7 | "bugs": "https://github.com/dkendal/pattern-match.js/issues", 8 | "repository": { 9 | "url": "https://github.com/dkendal/pattern-match.js.git", 10 | "type": "git" 11 | }, 12 | "keywords": [ 13 | "pattern", 14 | "matching", 15 | "functional", 16 | "typescript" 17 | ], 18 | "tags": [ 19 | "pattern", 20 | "matching", 21 | "functional", 22 | "typescript" 23 | ], 24 | "main": "dist/cjs/index.js", 25 | "module": "dist/esm/index.js", 26 | "types": "dist/esm/index.d.ts", 27 | "exports": { 28 | ".": { 29 | "import": "./dist/esm/index.js", 30 | "require": "./dist/cjs/index.js" 31 | } 32 | }, 33 | "sideEffects": false, 34 | "files": [ 35 | "dist/" 36 | ], 37 | "engines": { 38 | "node": ">=10" 39 | }, 40 | "scripts": { 41 | "test": "make test", 42 | "fmt": "yarn run dprint fmt", 43 | "clean": "make clean", 44 | "build": "make build", 45 | "build-release": "make build-release", 46 | "prepack": "make prepack" 47 | }, 48 | "dependencies": { 49 | "ts-toolbelt": "^9.6.0" 50 | }, 51 | "devDependencies": { 52 | "@skypack/package-check": "^0.2.2", 53 | "@tsconfig/node16": "^1.0.2", 54 | "@types/benchmark": "^2.1.1", 55 | "@types/jest": "^27.0.1", 56 | "@types/node": "^16.9.1", 57 | "benchmark": "^2.1.4", 58 | "chalk": "^5.0.0", 59 | "chalk-cli": "^5.0.0", 60 | "dprint": "^0.18.2", 61 | "esbuild": "^0.12.28", 62 | "esbuild-jest": "^0.5.0", 63 | "jest": "^27.2.0", 64 | "typescript": "^4.4.3" 65 | }, 66 | "workspaces": [ 67 | ".", 68 | "perf", 69 | "test/pack/*" 70 | ], 71 | "packageManager": "yarn@3.1.0" 72 | } 73 | -------------------------------------------------------------------------------- /perf/match.ts: -------------------------------------------------------------------------------- 1 | import { Suite } from 'benchmark' 2 | 3 | import * as ts from 'ts-pattern' 4 | import { __, match, V } from '../src/index' 5 | 6 | defsuite((suite) => { 7 | type Data = { type: 'text'; content: string } | { type: 'img'; src: string } 8 | 9 | type Result = { type: 'ok'; data: Data } | { type: 'error'; error: Error } 10 | 11 | const results: Result[] = [ 12 | { type: 'ok', data: { type: 'img', src: 'mock-src' } }, 13 | { type: 'error', error: new Error() }, 14 | { type: 'ok', data: { type: 'text', content: 'hello world' } }, 15 | ] 16 | 17 | for (const result of results) { 18 | { 19 | suite.add('@dkendal/match', () => { 20 | match( 21 | result, 22 | [{ type: 'error' }, () => `

Oups! An error occured

`], 23 | [ 24 | V('res', { type: 'ok', data: { type: 'text' } }), 25 | ({ res }) => `

${res.data.content}

`, 26 | ], 27 | [ 28 | { type: 'ok', data: { type: 'img', src: V('src') } }, 29 | ({ src }) => ``, 30 | ], 31 | ) 32 | }) 33 | 34 | suite.add('ts-pattern', () => { 35 | ts 36 | .match(result) 37 | .with({ type: 'error' }, (_res) => `

Oups! An error occured

`) 38 | .with( 39 | { type: 'ok', data: { type: 'text' } }, 40 | (res) => `

${res.data.content}

`, 41 | ) 42 | .with( 43 | { type: 'ok', data: { type: 'img', src: ts.select() } }, 44 | (src) => ``, 45 | ) 46 | .exhaustive() 47 | }) 48 | } 49 | } 50 | }) 51 | 52 | // defsuite((suite) => { 53 | // const value: unknown = []; 54 | 55 | // suite.add("match", () => { 56 | // match(value, [[], () => "empty"], [[V("hd"), __.tail], ({ hd }) => hd]); 57 | // }); 58 | 59 | // suite.add("explicit", () => { 60 | // if (typeof value === "object" && value instanceof Array) { 61 | // if (value === []) return "empty"; 62 | // else return value[0]; 63 | // } 64 | // }); 65 | // }); 66 | 67 | function defsuite(callback: (suite: Suite) => void) { 68 | const suite = new Suite() 69 | 70 | callback(suite) 71 | 72 | suite.on('cycle', function(event: any) { 73 | console.log(String(event.target)) 74 | }) 75 | 76 | suite.on('complete', function(this: any) { 77 | console.log('Fastest is ' + this.filter('fastest').map('name')) 78 | }) 79 | 80 | suite.run() 81 | } 82 | -------------------------------------------------------------------------------- /perf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perf", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "private": true, 8 | "dependencies": { 9 | "@dkendal/match": "workspace:*", 10 | "ts-pattern": "^3.3.3" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /perf/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig-base.json", 3 | 4 | "compilerOptions": { 5 | "lib": ["dom", "es2020"] 6 | }, 7 | 8 | "include": [ 9 | "*.ts" 10 | ], 11 | 12 | "references": [ 13 | { "path": "../src" } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /perf/yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 5 6 | cacheKey: 8 7 | 8 | "perf@workspace:.": 9 | version: 0.0.0-use.local 10 | resolution: "perf@workspace:." 11 | dependencies: 12 | ts-pattern: ^3.3.3 13 | languageName: unknown 14 | linkType: soft 15 | 16 | "ts-pattern@npm:^3.3.3": 17 | version: 3.3.3 18 | resolution: "ts-pattern@npm:3.3.3" 19 | checksum: 64c8535fbfc4f56bb9f00c4c4fa5a47471cc4e27c618cb25bc94df3e6f7fdaebff43be0071e117aa1d12ed0fa58ee960895b3311976c64b978559aac228ef0eb 20 | languageName: node 21 | linkType: hard 22 | -------------------------------------------------------------------------------- /src/const.ts: -------------------------------------------------------------------------------- 1 | export const __kind__ = Symbol.for('kind') 2 | export const __capture__ = Symbol.for('capture') 3 | 4 | export const any_ = 'any' 5 | export const rest_ = 'rest' 6 | export const string_ = 'string' 7 | export const number_ = 'number' 8 | export const boolean_ = 'boolean' 9 | export const function_ = 'function' 10 | export const object_ = 'object' 11 | export const bigint_ = 'bigint' 12 | export const symbol_ = 'symbol' 13 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { __, match, V, when } from './index' 2 | 3 | const ok = () => true 4 | const err = () => 'error' 5 | const id = (term: T): T => term 6 | const noMatch = Symbol.for('no match') 7 | 8 | describe('.match', () => { 9 | test('literal values', () => { 10 | const result = match(1, [1, ok]) 11 | 12 | expect(result).toBe(true) 13 | }) 14 | 15 | test('unmatched cases throw', () => { 16 | const fn = () => match(2, [1, ok]) 17 | 18 | expect(fn).toThrow('unmatched case: 2') 19 | }) 20 | 21 | test('multiple cases', () => { 22 | const result = match(2, [1, () => false], [2, ok]) 23 | 24 | expect(result).toBe(true) 25 | }) 26 | 27 | test('arrays', () => { 28 | const result = match( 29 | [1, 2, 3], 30 | // smaller 31 | [[1], () => 'err-1'], 32 | // larger 33 | [[1, 2, 3, 4], () => 'err-2'], 34 | // different 35 | [[2, 2, 3], () => 'err-3'], 36 | // ok 37 | [[1, 2, 3], ok], 38 | ) 39 | 40 | expect(result).toBe(true) 41 | }) 42 | 43 | test('nested arrays', () => { 44 | const result = match([1, [2, [3, 4, 5]]], [[1, [2, [3]]], () => false], [[ 45 | 1, 46 | [2, [3, 4, 5]], 47 | ], ok]) 48 | 49 | expect(result).toBe(true) 50 | }) 51 | 52 | test('objects', () => { 53 | const result = match({ a: 1, b: 2 }, [{ a: 2 }, () => false], [ 54 | { b: 2 }, 55 | ok, 56 | ]) 57 | 58 | expect(result).toBe(true) 59 | }) 60 | 61 | test('null values', () => { 62 | const result = match(null, [undefined, () => false], [null, ok]) 63 | 64 | expect(result).toBe(true) 65 | }) 66 | 67 | test('undefined', () => { 68 | const result = match(undefined, [null, () => false], [undefined, ok]) 69 | 70 | expect(result).toBe(true) 71 | }) 72 | 73 | test('nested objects', () => { 74 | const result = match( 75 | [{ a: { b: 1, c: 2 }, d: 3 }], 76 | when([{ a: { b: 1, c: 2 }, d: 3.1 }], () => false), 77 | when([{ a: { b: 1.1 } }], () => false), 78 | when([{ a: { b: 1 } }], ok), 79 | ) 80 | 81 | expect(result).toBe(true) 82 | }) 83 | 84 | test.each([ 85 | [1, when(__, ok)], 86 | [[1], when(__, ok)], 87 | [[1], when([__], ok)], 88 | [[1, 2, 3], when([__, 2, 3], ok)], 89 | [[1, 2, 3], when([__, __, 3], ok)], 90 | [[1, 2, 3], when([1, __, 3], ok)], 91 | [{ a: 1, b: 2 }, when({ a: __, b: 2 }, ok)], 92 | ])('untyped holes (%#)', (value, ...cases: any[]) => { 93 | const result = match(value, ...cases) 94 | expect(result).toBe(true) 95 | }) 96 | 97 | test.each([ 98 | [[1], when([__.rest], ok)], 99 | // matches 0 elements 100 | [[], when([__.rest], ok)], 101 | [[1, 2], when([__.rest], ok)], 102 | [[1, 2, 3], when([1, __.rest], ok)], 103 | [[1, 2, 3], when([1, 2, __.rest], ok)], 104 | ])('rest holes (%#)', (value, ...cases: any[]) => { 105 | const result = match(value, ...cases) 106 | expect(result).toBe(true) 107 | }) 108 | 109 | test('rest holes throw if there are multiple rests in an array', () => { 110 | const fn = () => match([1, 2, 3], when([__.rest, __.rest], ok)) 111 | expect(fn).toThrow() 112 | }) 113 | 114 | test.each([ 115 | ['', when(__.string, ok)], 116 | [null, when(__.string, err), when(__, ok)], 117 | 118 | [0, when(__.number, ok)], 119 | [null, when(__.number, err), when(__, ok)], 120 | 121 | [{}, when(__.object, ok)], 122 | [0, when(__.object, err), when(__, ok)], 123 | 124 | [id, when(__.function, ok)], 125 | [0, when(__.function, err), when(__, ok)], 126 | ])('typed holes holes (%#)', (value, ...cases: any[]) => { 127 | const result = match(value, ...cases) 128 | expect(result).toBe(true) 129 | }) 130 | 131 | test.each([ 132 | [1, V('x'), { x: 1 }], 133 | [1, V('x', __), { x: 1 }], 134 | [1, V('x', __.number), { x: 1 }], 135 | [1, V('x', __.string), noMatch], 136 | [{ a: 1, b: 2 }, { a: V('x') }, { x: 1 }], 137 | [[1, 2, 3], [V('hd'), __.rest], { hd: 1 }], 138 | // TODO [[1, 2, 3], [__, V("tl")(__.tail)], { tl: [2, 3] }], 139 | ])('variable capture: %p (%#)', (value: any, pattern: any, expected: any) => { 140 | expect(match(value, [pattern, id], [__, () => noMatch])).toEqual(expected) 141 | }) 142 | 143 | test('no variables are captured by default', () => { 144 | expect(match(1, [__, id])).toEqual({}) 145 | }) 146 | 147 | test.todo('rest holes throw if used outside of an array') 148 | }) 149 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { A, F, L } from 'ts-toolbelt' 2 | 3 | import { 4 | __capture__, 5 | __kind__, 6 | any_, 7 | bigint_, 8 | boolean_, 9 | function_, 10 | number_, 11 | object_, 12 | rest_, 13 | string_, 14 | symbol_, 15 | } from './const' 16 | 17 | import type { 18 | Capture, 19 | CaseParameters, 20 | Hole, 21 | Kind, 22 | PatternHandler, 23 | } from './types' 24 | 25 | function hole(kind: Label): Hole { 26 | return { [__kind__]: kind } as any 27 | } 28 | 29 | function capture( 30 | name: Name, 31 | ): Capture 32 | 33 | function capture( 34 | name: Name, 35 | pattern: F.Narrow, 36 | ): Capture 37 | 38 | function capture(name: any, pattern: any = holes.any): Capture { 39 | return Object.freeze({ [__capture__]: name, pattern }) 40 | } 41 | 42 | export const V = capture 43 | 44 | function isCapture(term: unknown): term is Capture { 45 | return isNonPrimitive(term) && __capture__ in term 46 | } 47 | 48 | function isHole(term: unknown): term is Hole { 49 | return typeof term === 'object' && term !== null && __kind__ in term 50 | } 51 | 52 | function isSpecial(term: unknown): term is Hole | Capture { 53 | return isNonPrimitive(term) && (__kind__ in term || __capture__ in term) 54 | } 55 | 56 | function testHole(hole: Hole, term: unknown): boolean { 57 | if (!(__kind__ in hole)) throw 'ni' 58 | 59 | const holeKind = hole[__kind__] 60 | 61 | switch (holeKind) { 62 | // FIXME this doesn't really work like the others it only receives one of 63 | // the "rest" values, but has logic that disables checking that arrays 64 | // are the same length. this is more imporant if we want to capture 65 | // values. 66 | case rest_: 67 | case any_: 68 | return true 69 | default: 70 | return typeof term === holeKind 71 | } 72 | } 73 | 74 | // This little interface dance is done in the name of "readability" (debatable) 75 | 76 | interface __any extends Hole {} 77 | const __any: __any = hole(any_) 78 | 79 | export interface __string extends Hole {} 80 | const __string: __string = hole(string_) 81 | 82 | export interface __number extends Hole {} 83 | const __number: __number = hole(number_) 84 | 85 | export interface __boolean extends Hole {} 86 | const __boolean: __boolean = hole(boolean_) 87 | 88 | export interface __symbol extends Hole {} 89 | const __symbol: __symbol = hole(symbol_) 90 | 91 | export interface __bigint extends Hole {} 92 | const __bigint: __bigint = hole(bigint_) 93 | 94 | export interface __object extends Hole {} 95 | const __object: __object = hole(object_) 96 | 97 | export interface __function extends Hole {} 98 | const __function: __function = hole(function_) 99 | 100 | export interface __rest extends Hole {} 101 | const __rest: __rest = hole(rest_) 102 | 103 | export const holes = { 104 | string: __string, 105 | number: __number, 106 | boolean: __boolean, 107 | object: __object, 108 | function: __function, 109 | symbol: __symbol, 110 | bigint: __bigint, 111 | 112 | // This is intentionally not the same object as __ to prevent 113 | // a circular reference 114 | any: __any, 115 | 116 | rest: __rest, 117 | // Aliases 118 | tail: __rest, 119 | tl: __rest, 120 | } 121 | 122 | export interface __ extends Hole { 123 | string: __string 124 | number: __number 125 | boolean: __boolean 126 | // There is something weird going on where including this causes 127 | // RecursiveCollect to think that it has a circular reference 128 | object: __object 129 | function: __function 130 | symbol: __symbol 131 | bigint: __bigint 132 | any: __any 133 | rest: __rest 134 | tail: __rest 135 | tl: __rest 136 | } 137 | 138 | /** 139 | * A hole for any value, and an object that has references 140 | * to the other type holes. 141 | */ 142 | // TODO better syntax for function object 143 | export const __: __ = Object.assign(hole(any_), holes) 144 | 145 | export function when>( 146 | a: Pattern, 147 | b: Handler, 148 | ): [Pattern, Handler] { 149 | return [a, b] 150 | } 151 | 152 | export function match< 153 | Value, 154 | Patterns extends unknown[], 155 | Returns extends [unknown, unknown][], 156 | Cases extends { 157 | [K in keyof Returns]: Returns[K] extends [unknown, unknown] 158 | ? [Returns[K][0], (args: never) => Returns[K][1]] 159 | : never 160 | }, 161 | >( 162 | value: Value, 163 | ...cases: 164 | & { 165 | [K in keyof Patterns]: [ 166 | Patterns[K], 167 | (args: CaseParameters) => any, 168 | ] 169 | } 170 | & Cases 171 | ): L.UnionOf< 172 | A.Cast<{ [K in keyof Cases]: F.Return }, readonly any[]> 173 | > 174 | 175 | export function match(valueRoot: any, ...cases: any[]): any { 176 | // TODO error on no cases 177 | next_case: 178 | for (let [patternRoot, handler] of cases) { 179 | const captures: Record = {} 180 | // TODO type guards (functions) 181 | // if (typeof patternRoot === "function") 182 | 183 | // Treat nulls like built-in types despite being objects 184 | if (isNonPrimitive(patternRoot)) { 185 | // This is a stack based breadth-first-traversal of the "expected" and 186 | // "actual" objects in tandem. First the node from the "expected" object 187 | // is pushed on to the stack, along with the node in the same position 188 | // from the "actual" object. While comparing nodes, if a note a leaf but 189 | // is a node that needs to be descended into the nodes for each tree are 190 | // pushed onto the stack which alleviates the need for a recursive call, 191 | // additional function creation on the heap, etc. 192 | const stack = [[patternRoot, valueRoot]] 193 | while (stack.length > 0) { 194 | const [pnode, vnode] = stack.pop() as [any, any] 195 | 196 | if (isCapture(pnode)) { 197 | if ('pattern' in pnode) stack.push([pnode.pattern, vnode]) 198 | captures[pnode[__capture__]] = vnode 199 | continue 200 | } 201 | 202 | if (isHole(pnode)) { 203 | if (testHole(pnode, vnode)) continue 204 | else continue next_case 205 | } 206 | 207 | // Special case for arrays 208 | if ( 209 | pnode instanceof Array 210 | && pnode.length !== vnode.length 211 | // If the list has any elements and the last is not a "rest" 212 | && (pnode.length === 0 || pnode[pnode.length - 1] !== __rest) 213 | ) { 214 | continue next_case 215 | } 216 | 217 | const lastIdx = (pnode.length - 1).toString() 218 | 219 | // TODO: be more careful about dealing with prototypes 220 | for (let [attr, pchild] of Object.entries(pnode)) { 221 | // Check if there is a malformed "rest" 222 | if (pchild === __rest && attr !== lastIdx) 223 | throw 'rest must be the last element of an array' 224 | 225 | const vchild = vnode[attr] 226 | 227 | // Push node to handle at the top of the loop 228 | if (isSpecial(pchild)) stack.push([pchild, vchild]) 229 | else if (typeof pchild !== typeof vchild) continue next_case 230 | // Push node onto stack to descend into 231 | else if (typeof pchild === 'object') stack.push([pchild, vchild]) 232 | // Leaf comparison 233 | else if (vchild !== pchild) continue next_case 234 | } 235 | } 236 | return handler(captures) 237 | } 238 | 239 | // maybe this can be folded into the logic above 240 | // Leaf comparison 241 | if (patternRoot !== valueRoot) continue next_case 242 | return handler(captures) 243 | } 244 | 245 | throw `unmatched case: ${JSON.stringify(valueRoot)}` 246 | } 247 | 248 | type NonPrimitive = Function | Object 249 | 250 | function isNonPrimitive(term: unknown): term is NonPrimitive { 251 | const t = typeof term 252 | return (t === 'object' || t === 'function') && term !== null 253 | } 254 | -------------------------------------------------------------------------------- /src/tsconfig-release-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "rootDir": ".", 6 | "outDir": "../dist/cjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/tsconfig-release-esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "module": "es6", 6 | "rootDir": ".", 7 | "outDir": "../dist/esm" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig-base", 3 | 4 | "include": ["*.ts"], 5 | "exclude": ["*.test.ts"] 6 | } 7 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { A, B, I, M, Union } from "ts-toolbelt"; 2 | 3 | import type { __ } from "."; 4 | 5 | import type { 6 | __capture__, 7 | __kind__, 8 | any_, 9 | bigint_, 10 | boolean_, 11 | function_, 12 | number_, 13 | object_, 14 | rest_, 15 | string_, 16 | symbol_, 17 | } from "./const"; 18 | 19 | type True = 1; 20 | 21 | type ExtractSubcapture = T extends M.Primitive | M.BuiltIn 22 | ? never 23 | : T extends object 24 | ? T[Exclude] 25 | : never; 26 | 27 | type PartialAssignment = V extends never 28 | ? never 29 | : K extends string 30 | ? { [k in K]: V } 31 | : never; 32 | 33 | type EmptyToNever = {} extends T ? never : T; 34 | 35 | export type Kind = 36 | | typeof any_ 37 | | typeof rest_ 38 | | typeof string_ 39 | | typeof number_ 40 | | typeof boolean_ 41 | | typeof function_ 42 | | typeof symbol_ 43 | | typeof bigint_ 44 | | typeof object_; 45 | 46 | export interface Hole { 47 | T: Type; 48 | readonly [__kind__]: Label; 49 | } 50 | 51 | export type HoleInnerType = T extends Hole ? U : T; 52 | 53 | export interface Capture { 54 | readonly [__capture__]: Name; 55 | readonly pattern: Pattern; 56 | } 57 | 58 | type CapturePattern = T extends Capture ? Pattern : T; 59 | 60 | type RecursiveExpandCapture = A.Equals extends True 61 | ? T 62 | : T extends Hole | Capture 63 | ? ExpandCapture 64 | : T extends Record 65 | ? { [K in keyof T]: ExpandCapture } 66 | : never; 67 | 68 | export type ExpandCapture = RecursiveExpandCapture< 69 | CapturePattern> 70 | >; 71 | 72 | type NeverToEmpty = [T] extends [never] ? {} : T; 73 | 74 | type Explode = T[keyof T]; 75 | 76 | export type RecursiveCollect = NeverToEmpty>; 77 | 78 | type IsLeafNode = B.Or< 79 | B.Not>>, 80 | B.Or, A.Equals> 81 | >; 82 | 83 | type WalkChildren = Explode< 84 | { [Key in keyof Node]: DoRecursiveCollect } 85 | >; 86 | 87 | type DoRecursiveCollect = IsLeafNode extends True 88 | ? never 89 | : Node extends Capture 90 | ? Node | DoRecursiveCollect

91 | : WalkChildren; 92 | 93 | type UnionizeCapture = T extends Capture 94 | ? K extends string 95 | ? { [k in K]: V } 96 | : never 97 | : never; 98 | 99 | export type CaptureToEntry = Union.Merge>; 100 | 101 | export type VariableCapture = ExpandCapture< 102 | CaptureToEntry> 103 | >; 104 | 105 | export type PatternHandler = ( 106 | arg1: VariableCapture 107 | ) => unknown; 108 | 109 | type IsTopType = A.Extends< 110 | True, 111 | | A.Equals 112 | | A.Equals 113 | | A.Equals 114 | | A.Equals 115 | >; 116 | 117 | export type UnifyAll = IsTopType extends True 118 | ? VariableCapture 119 | : Overlapping> extends {} 120 | ? DoUnifyAll 121 | : never; 122 | 123 | export type DoUnifyAll = Pattern extends Capture< 124 | infer VarName, 125 | infer Child 126 | > 127 | ? 128 | | PartialAssignment>> 129 | | UnifyAll 130 | : Overlapping> extends object 131 | ? ExtractSubcapture< 132 | { 133 | [K in keyof Value]: K extends keyof Pattern 134 | ? UnifyAll 135 | : never; 136 | } 137 | > 138 | : never; 139 | 140 | export type ShouldUnifyAll = B.Not< 141 | B.Or< 142 | A.Equals & A.Equals, 143 | A.Extends & A.Extends 144 | > 145 | >; 146 | 147 | export type CaseParameters = EmptyToNever< 148 | Union.Merge> 149 | >; 150 | 151 | export type Overlapping< 152 | A, 153 | B, 154 | Depth extends I.Iteration = I.IterationOf<0> 155 | > = A.Equals extends True 156 | ? B 157 | : A extends B 158 | ? A 159 | : [A, B, keyof A] extends 160 | | [Array, Array, any] 161 | | [object, object, keyof B] 162 | ? RejectMismatch< 163 | { 164 | [Key in keyof A]: Key extends keyof B 165 | ? Overlapping> 166 | : A[Key]; 167 | } 168 | > 169 | : never; 170 | 171 | export type RejectMismatch = {} extends T 172 | ? never 173 | : T extends AnyNever> 174 | ? never 175 | : T; 176 | 177 | export type AnyNever = keyof T extends infer S 178 | ? S extends any 179 | ? { [K in A.Cast]: never } 180 | : never 181 | : never; 182 | -------------------------------------------------------------------------------- /src/types.nt: -------------------------------------------------------------------------------- 1 | #lang dts 2 | 3 | global (Exclude String Record Object Array) 4 | 5 | import "ts-toolbelt" (A B I M Union) 6 | 7 | import "." ( __ ) 8 | 9 | import "./const" ( __capture__ 10 | __kind__ 11 | any_ 12 | bigint_ 13 | boolean_ 14 | function_ 15 | number_ 16 | object_ 17 | rest_ 18 | string_ 19 | symbol_ ) 20 | 21 | type True = 1 22 | 23 | type ExtractSubcapture T = 24 | match T with 25 | | (M.Primitive | M.BuiltIn) -> never 26 | | object -> T[(Exclude (keyof T) ((keyof []) | (keyof {})))] 27 | 28 | type PartialAssignment K V = 29 | if V !<: never and K <: string then { k <- K | k: V } 30 | 31 | type EmptyToNever T = 32 | if {} !<: T then T 33 | 34 | export type Kind = 35 | | (typeof any_) 36 | | (typeof rest_) 37 | | (typeof string_) 38 | | (typeof number_) 39 | | (typeof boolean_) 40 | | (typeof function_) 41 | | (typeof symbol_) 42 | | (typeof bigint_) 43 | | (typeof object_) 44 | 45 | export interface Hole (Type = any) (Label = any) = { 46 | T: Type 47 | readonly [__kind__]: Label 48 | } 49 | 50 | export type HoleInnerType T = 51 | if T <: (Hole ^U) then U else T 52 | 53 | export interface Capture (Name <: String = any) (Pattern = any) = { 54 | readonly [__capture__]: Name 55 | readonly pattern: Pattern 56 | } 57 | 58 | type CapturePattern T = 59 | if T <: (Capture any ^Pattern) then Pattern 60 | else T 61 | 62 | type RecursiveExpandCapture T = 63 | -- TODO add equals operator 64 | if (A.Equals T any) <: True then T 65 | else match T with 66 | | (Hole | Capture) -> (ExpandCapture T) 67 | | (Record string any) -> { K <- (keyof T) | K: (ExpandCapture (T[K])) } 68 | 69 | export type ExpandCapture T = 70 | (RecursiveExpandCapture (CapturePattern (HoleInnerType T))) 71 | 72 | type NeverToEmpty T = 73 | if T == never then {} else T 74 | 75 | type Explode T = T[(keyof T)] 76 | 77 | export type RecursiveCollect Node = 78 | (NeverToEmpty (DoRecursiveCollect Node)) 79 | 80 | type IsLeafNode T = 81 | (B.Or 82 | (B.Not (A.Extends T (Record string any))) 83 | (B.Or (A.Extends T Hole) (A.Equals T any))) 84 | 85 | type WalkChildren Node = 86 | (Explode { Key <- (keyof Node) 87 | | Key: (DoRecursiveCollect Node[Key]) 88 | }) 89 | 90 | type DoRecursiveCollect Node = 91 | if (IsLeafNode Node) <: True then never 92 | else if Node <: (Capture any ^P) then Node | (DoRecursiveCollect P) 93 | else (WalkChildren Node) 94 | 95 | type UnionizeCapture T = 96 | if T <: (Capture ^K ^V) then if K <: string then { k <- K | k: V } 97 | 98 | export type CaptureToEntry T = 99 | (Union.Merge (UnionizeCapture T)) 100 | 101 | export type VariableCapture T = 102 | (ExpandCapture (CaptureToEntry (RecursiveCollect T))) 103 | 104 | export type PatternHandler Pattern = 105 | (VariableCapture Pattern) => unknown 106 | 107 | type IsTopType T = 108 | let top = | (A.Equals T unknown) 109 | | (A.Equals T object) 110 | | (A.Equals T Object) 111 | | (A.Equals T any) 112 | in (A.Extends True top) 113 | 114 | export type UnifyAll Value Pattern = 115 | if (IsTopType Value) <: True then (VariableCapture Pattern) 116 | else if (Overlapping Value (ExpandCapture Pattern)) <: {} 117 | then (DoUnifyAll Value Pattern) 118 | 119 | export type DoUnifyAll Value Pattern = 120 | let overlap x = (Overlapping Value (ExpandCapture x)) in 121 | if Pattern <: (Capture ^VarName ^Child) 122 | then | (PartialAssignment VarName (overlap Child)) 123 | | (UnifyAll Value Child) 124 | else if (overlap Pattern) <: object 125 | then (ExtractSubcapture 126 | { K <- (keyof Value) 127 | | K : if K <: (keyof Pattern) then (UnifyAll Value[K] Pattern[K]) 128 | }) 129 | 130 | 131 | export type ShouldUnifyAll Value Pattern = 132 | (B.Not 133 | (B.Or 134 | (A.Equals Pattern any) & (A.Equals Value any) 135 | (A.Extends Pattern M.Primitive) & (A.Extends Value M.Primitive))) 136 | 137 | export type CaseParameters Value Pattern = 138 | (EmptyToNever (Union.Merge (UnifyAll Value Pattern))) 139 | 140 | export type Overlapping A B (Depth <: I.Iteration = (I.IterationOf 0)) = 141 | if (A.Equals A any) <: True then B 142 | else if A <: B then A 143 | else if [A B (keyof A)] <: | [(Array any) (Array any) any] 144 | | [object object (keyof B)] 145 | then (RejectMismatch 146 | { Key <- (keyof A) 147 | | Key : 148 | if Key <: (keyof B) then (Overlapping A[Key] B[Key] (I.Next Depth)) 149 | else A[Key] 150 | }) 151 | 152 | export type RejectMismatch T = 153 | if {} !<: T and T !<: (AnyNever (A.Cast T object)) then T 154 | 155 | export type AnyNever (T <: object) = 156 | if (keyof T) <: ^S and S <: any 157 | then { K <- (A.Cast S (string | number | symbol)) 158 | | K : never } 159 | 160 | -- vi: syntax=haskell: 161 | -------------------------------------------------------------------------------- /test/cases/conformance/expand-capture.ts: -------------------------------------------------------------------------------- 1 | import { __ } from 'src' 2 | import { Capture, ExpandCapture } from 'src/types' 3 | import { A } from 'ts-toolbelt' 4 | import { check, checks, Pass } from 'ts-toolbelt/out/Test' 5 | import { Data, Result, Success, Text } from './test-helper' 6 | 7 | checks([ 8 | check< 9 | ExpandCapture< 10 | { type: 'ok'; data: { type: 'img'; src: Capture<'src', __> } } 11 | >, 12 | { type: 'ok'; data: { type: 'img'; src: any } }, 13 | Pass 14 | >(), 15 | ]) 16 | -------------------------------------------------------------------------------- /test/cases/conformance/match.ts: -------------------------------------------------------------------------------- 1 | import { __, match, V } from 'src' 2 | import { check, checks, Pass } from 'ts-toolbelt/out/Test' 3 | import { Data, Result, Success, Text, valueof } from './test-helper' 4 | 5 | // Top types 6 | match(valueof(), [ 7 | V('value'), 8 | // valueof>(), 9 | (arg) => checks([check()]), 10 | ]) 11 | 12 | match(valueof<{}>(), [ 13 | V('value'), 14 | // valueof>(), 15 | (arg) => checks([check()]), 16 | ]) 17 | 18 | match(valueof(), [ 19 | V('value'), 20 | // valueof>(), 21 | (arg) => checks([check()]), 22 | ]) 23 | 24 | // Primitives 25 | 26 | match(valueof(), [ 27 | V('value'), 28 | // valueof>(), 29 | (arg) => checks([check()]), 30 | ]) 31 | 32 | // Return type is implied, single case 33 | { 34 | const returnValue = match(valueof<{ x: 1 }>(), [ 35 | V('val'), 36 | (arg) => { 37 | checks([check()]) 38 | return 1 39 | }, 40 | ]) 41 | 42 | checks([check()]) 43 | } 44 | 45 | // Return type is implied, union of all other branches return values 46 | { 47 | const returnValue = match( 48 | valueof(), 49 | [ 50 | V('str', __.string), 51 | (arg) => { 52 | checks([check()]) 53 | return arg.str 54 | }, 55 | ], 56 | [ 57 | V('num', __.number), 58 | (arg) => { 59 | checks([check()]) 60 | return arg.num 61 | }, 62 | ], 63 | [ 64 | V('bool', __.boolean), 65 | (arg) => { 66 | checks([check()]) 67 | return arg.bool 68 | }, 69 | ], 70 | ) 71 | 72 | checks([check()]) 73 | } 74 | 75 | match(valueof>(), [ 76 | { value: V('value') }, 77 | (arg) => { 78 | checks([check()]) 79 | }, 80 | ]) 81 | 82 | match(valueof>(), [ 83 | { value: { id: V('capture_id') } }, 84 | (arg) => { 85 | checks([check()]) 86 | }, 87 | ]) 88 | 89 | match(valueof>(), [ 90 | { error: V('error') }, 91 | (arg) => { 92 | checks([check()]) 93 | }, 94 | ]) 95 | 96 | match(valueof>(), [ 97 | { value: V('cValue', { id: V('cId') }) }, 98 | (arg) => { 99 | checks([ 100 | check(), 101 | ]) 102 | }, 103 | ]) 104 | 105 | match(valueof>(), [ 106 | { value: V('value', { id: V('id', __.number) }) }, 107 | (arg) => { 108 | checks([check()]) 109 | }, 110 | ]) 111 | 112 | match(valueof>(), [ 113 | { value: V('value', { id: V('id', __.string) }) }, 114 | (arg) => { 115 | checks([check()]) 116 | }, 117 | ]) 118 | 119 | match(valueof>(), [ 120 | { value: V('value', { id: V('id', __.string) }) }, 121 | (arg) => { 122 | checks([check()]) 123 | return 1 124 | }, 125 | ]) 126 | 127 | // Object top type 128 | match(valueof(), [ 129 | V('value', { x: __ }), 130 | (arg) => { 131 | checks([check()]) 132 | }, 133 | ]) 134 | 135 | match(valueof(), [ 136 | V('value', { x: __ }), 137 | (arg) => { 138 | checks([check()]) 139 | }, 140 | ]) 141 | 142 | // Top type 143 | match(valueof(), [ 144 | V('value', __), 145 | (arg) => { 146 | checks([check()]) 147 | }, 148 | ]) 149 | 150 | match(valueof(), [ 151 | V('value', __.string), 152 | (arg) => { 153 | checks([check()]) 154 | }, 155 | ]) 156 | 157 | // Regressions 158 | 159 | match( 160 | valueof>(), 161 | [{ type: 'error' }, (args) => checks([check()])], 162 | [ 163 | V('res', { type: 'ok', value: { type: 'text' } }), 164 | args => checks([check }, 1>()]), 165 | ], 166 | [ 167 | { type: 'ok', value: { type: 'img', src: V('src') } }, 168 | args => checks([check()]), 169 | ], 170 | ) 171 | -------------------------------------------------------------------------------- /test/cases/conformance/overlapping.ts: -------------------------------------------------------------------------------- 1 | import { __ } from 'src' 2 | import { Capture, ExpandCapture, Overlapping } from 'src/types' 3 | import { A } from 'ts-toolbelt' 4 | import { check, checks, Pass } from 'ts-toolbelt/out/Test' 5 | import { Data, Img, Result, Success, Text } from './test-helper' 6 | 7 | checks([ 8 | // Top Types 9 | check, {}, Pass>(), // ?? 10 | check, {}, Pass>(), 11 | check, never, Pass>(), 12 | check, {}, Pass>(), 13 | check, any, Pass>(), 14 | check, string, Pass>(), 15 | check, unknown, Pass>(), 16 | check, never, Pass>(), 17 | check, never, Pass>(), 18 | 19 | // Literals 20 | check, 1, Pass>(), 21 | check, 'a', Pass>(), 22 | 23 | // Primitives 24 | check, string, Pass>(), 25 | check, number, Pass>(), 26 | check, boolean, Pass>(), 27 | 28 | // Empty Array 29 | check, [], Pass>(), 30 | check, never, Pass>(), 31 | check, [], Pass>(), 32 | 33 | // Objects 34 | check, {}, Pass>(), 35 | check, { x: 0 }, Pass>(), 36 | check, never, Pass>(), 37 | 38 | // Tuples 39 | check, never, Pass>(), 40 | check, [0, 1, 2], Pass>(), 41 | check, ['a'], Pass>(), 42 | 43 | // Primitive Unions 44 | check< 45 | Overlapping, 46 | string, 47 | Pass 48 | >(), 49 | check, 1, Pass>(), 50 | 51 | // Unions of tuples 52 | check< 53 | Overlapping<['ok', string] | ['error', Error], ['ok', any]>, 54 | ['ok', string], 55 | Pass 56 | >(), 57 | 58 | // Unions of objects 59 | check< 60 | Overlapping< 61 | Result, 62 | { type: 'ok' } 63 | >, 64 | Success, 65 | Pass 66 | >(), 67 | 68 | // Unions of objects no shared properties 69 | check< 70 | Overlapping< 71 | { a: 'a' } | { b: 'b' }, 72 | { a: 'a' } 73 | >, 74 | { a: 'a' }, 75 | Pass 76 | >(), 77 | 78 | // Union as a property 79 | check< 80 | Overlapping< 81 | { result: Result }, 82 | { result: { type: 'ok' } } 83 | >, 84 | { result: Success }, 85 | Pass 86 | >(), 87 | 88 | // Nested union as property 89 | check< 90 | Overlapping< 91 | { result: Result }, 92 | { result: Success<{ type: 'text' }> } 93 | >, 94 | { result: Success }, 95 | Pass 96 | >(), 97 | 98 | check< 99 | Overlapping< 100 | { data: { a: 'a'; b: 'b' } | { c: 'c'; d: 'd' } }, 101 | { data: { a: 'a' } } 102 | >, 103 | { data: { a: 'a'; b: 'b' } }, 104 | Pass 105 | >(), 106 | 107 | check< 108 | Overlapping< 109 | Result, 110 | ExpandCapture<{ type: 'ok'; value: { type: 'img'; src: Capture<'src', __> } }> 111 | >, 112 | Success, 113 | Pass 114 | >(), 115 | 116 | // Deeply nested union 117 | check< 118 | A.Compute< 119 | Overlapping< 120 | Result>>, 121 | Success>> 122 | > 123 | >, 124 | Success>>, 125 | Pass 126 | >(), 127 | ]) 128 | -------------------------------------------------------------------------------- /test/cases/conformance/test-helper.ts: -------------------------------------------------------------------------------- 1 | export declare function valueof(): T 2 | 3 | export type Result = Success | Failure 4 | export type Success = { type: 'ok'; value: T } 5 | export type Failure = { type: 'error'; error: Error } 6 | export type Text = { type: 'text'; content: string } 7 | export type Img = { type: 'img'; src: string } 8 | export type Data = Text | Img 9 | -------------------------------------------------------------------------------- /test/cases/conformance/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | 4 | "extends": "../../../tsconfig-base", 5 | 6 | "compilerOptions": { 7 | "baseUrl": "../../..", 8 | "noUnusedLocals": false, 9 | "noEmit": true, 10 | "noErrorTruncation": true 11 | }, 12 | 13 | "include": [ 14 | "*.ts" 15 | ], 16 | 17 | "references": [ 18 | { "path": "../../../src" } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/cases/conformance/unify-all.ts: -------------------------------------------------------------------------------- 1 | import { __ } from 'src' 2 | import { Capture, DoUnifyAll, UnifyAll } from 'src/types' 3 | import { check, checks, Pass } from 'ts-toolbelt/out/Test' 4 | import { Data, Img, Result, Success, Text } from './test-helper' 5 | 6 | checks([ 7 | // Holes 8 | check< 9 | UnifyAll>, 10 | { value: string }, 11 | Pass 12 | >(), 13 | 14 | check< 15 | UnifyAll>, 16 | { value: any }, 17 | Pass 18 | >(), 19 | 20 | check< 21 | UnifyAll>, 22 | { value: string }, 23 | Pass 24 | >(), 25 | 26 | check< 27 | UnifyAll>, 28 | never, 29 | Pass 30 | >(), 31 | 32 | // Top types 33 | check>, { value: {} }, Pass>(), 34 | 35 | // Primitives 36 | // Literals 37 | check, never, Pass>(), 38 | check, never, Pass>(), 39 | 40 | check, never, Pass>(), 41 | check, never, Pass>(), 42 | check, never, Pass>(), 43 | check, never, Pass>(), 44 | 45 | check, never, Pass>(), 46 | check, never, Pass>(), 47 | check, never, Pass>(), 48 | check {}, any>, never, Pass>(), 49 | 50 | check< 51 | UnifyAll<'string', Capture<'value', any>>, 52 | { value: 'string' }, 53 | Pass 54 | >(), 55 | check>, { value: 1 }, Pass>(), 56 | 57 | // Objects 58 | check< 59 | UnifyAll }>, 60 | { value: string }, 61 | Pass 62 | >(), 63 | check< 64 | UnifyAll, { error: Capture<'value'> }>, 65 | { value: Error }, 66 | Pass 67 | >(), 68 | check< 69 | UnifyAll, { value: Capture<'value'> }>, 70 | { value: Text }, 71 | Pass 72 | >(), 73 | 74 | // Numbers 75 | check< 76 | UnifyAll<{ x: 1 }, Capture<'value', any>>, 77 | { value: { x: 1 } }, 78 | Pass 79 | >(), 80 | check< 81 | UnifyAll<{ x: 1 }, Capture<'value', {}>>, 82 | { value: { x: 1 } }, 83 | Pass 84 | >(), 85 | check< 86 | UnifyAll<{ x: 1 }, Capture<'value', { x: 1 }>>, 87 | { value: { x: 1 } }, 88 | Pass 89 | >(), 90 | 91 | // Strings 92 | check< 93 | UnifyAll<{ type: 'ok' }, Capture<'value', any>>, 94 | { value: { type: 'ok' } }, 95 | Pass 96 | >(), 97 | check< 98 | UnifyAll<{ type: 'ok' }, Capture<'value', {}>>, 99 | { value: { type: 'ok' } }, 100 | Pass 101 | >(), 102 | check< 103 | UnifyAll<{ type: 'ok' }, Capture<'value', { type: 'ok' }>>, 104 | { value: { type: 'ok' } }, 105 | Pass 106 | >(), 107 | 108 | check< 109 | UnifyAll<{ type: 'ok' }, Capture<'value', { type: 'ok' }>>, 110 | { value: { type: 'ok' } }, 111 | Pass 112 | >(), 113 | 114 | // Union 115 | check< 116 | UnifyAll>, 117 | { value: { type: 'text'; content: string } }, 118 | Pass 119 | >(), 120 | 121 | check< 122 | UnifyAll< 123 | 'a' | 'b', 124 | Capture<'value', 'a'> 125 | >, 126 | { value: 'a' }, 127 | Pass 128 | >(), 129 | 130 | // Child union (array) 131 | check< 132 | UnifyAll< 133 | ['a' | 'b'], 134 | Capture<'value', ['a']> 135 | >, 136 | { value: ['a'] }, 137 | Pass 138 | >(), 139 | 140 | // Child union (object) 141 | check< 142 | UnifyAll< 143 | { data: { a: 'a'; b: 'b' } | { c: 'c'; d: 'd' } }, 144 | Capture<'value', { data: { a: 'a' } }> 145 | >, 146 | { value: { data: { a: 'a'; b: 'b' } } }, 147 | Pass 148 | >(), 149 | 150 | // Child union 151 | check< 152 | UnifyAll< 153 | { 154 | pet: { type: 'cat'; breed: 'tabby' } | { type: 'dog'; breed: 'collie' } 155 | }, 156 | Capture<'value', { pet: { type: 'dog' } }> 157 | >, 158 | { value: { pet: { type: 'dog'; breed: 'collie' } } }, 159 | Pass 160 | >(), 161 | 162 | // Nested Union 163 | check< 164 | UnifyAll< 165 | Result, 166 | Capture<'value', { type: 'ok'; value: { type: 'text' } }> 167 | >, 168 | { value: Success }, 169 | Pass 170 | >(), 171 | 172 | check< 173 | UnifyAll< 174 | { x: string }, 175 | { x: Capture<'x', __> } 176 | >, 177 | { x: string }, 178 | Pass 179 | >(), 180 | 181 | check< 182 | UnifyAll< 183 | { x: string | number }, 184 | { x: Capture<'x', __> } 185 | >, 186 | { x: string } | { x: number }, 187 | Pass 188 | >(), 189 | 190 | check< 191 | DoUnifyAll< 192 | { a: 1 } | { b: 2 }, 193 | { a: Capture<'a', __> } 194 | >, 195 | { a: 1 }, 196 | Pass 197 | >(), 198 | 199 | check< 200 | UnifyAll< 201 | Img, 202 | { type: 'img'; src: Capture<'src', __> } 203 | >, 204 | { src: string }, 205 | Pass 206 | >(), 207 | 208 | check< 209 | UnifyAll< 210 | Data, 211 | { type: 'img'; src: Capture<'src', __> } 212 | >, 213 | { src: string }, 214 | Pass 215 | >(), 216 | 217 | check< 218 | DoUnifyAll< 219 | Result, 220 | { type: 'ok'; value: { type: 'img'; src: Capture<'src', __> } } 221 | >, 222 | { src: string }, 223 | Pass 224 | >(), 225 | 226 | check< 227 | UnifyAll< 228 | Result>>, 229 | Capture<'value', Success>>> 230 | >, 231 | { value: Success>> }, 232 | Pass 233 | >(), 234 | ]) 235 | -------------------------------------------------------------------------------- /test/cases/conformance/variable-capture.ts: -------------------------------------------------------------------------------- 1 | import { __, __function, __object, __string } from 'src' 2 | import type { 3 | Capture as $, 4 | CaptureToEntry, 5 | ExpandCapture, 6 | Hole, 7 | HoleInnerType, 8 | RecursiveCollect, 9 | VariableCapture, 10 | } from 'src/types' 11 | import { Test } from 'ts-toolbelt' 12 | 13 | // Just for testing clarity 14 | type $a = $<'a', T> 15 | type $b = $<'b', T> 16 | type $c = $<'c', T> 17 | 18 | const { check, checks } = Test 19 | type Pass = Test.Pass 20 | 21 | checks([ 22 | check, any, Pass>(), 23 | check, string, Pass>(), 24 | check, number, Pass>(), 25 | check, any[], Pass>(), 26 | ]) 27 | 28 | checks([ 29 | check, { a: any }, Pass>(), 30 | check>, { a: {} }, Pass>(), 31 | check>, { a: 1 }, Pass>(), 32 | check>, { a: { x: 1 } }, Pass>(), 33 | check, { a: any; b: any; c: any }, Pass>(), 34 | check< 35 | CaptureToEntry<$a<{ b: $b<1> }> | $b<1>>, 36 | { a: { b: $b<1> }; b: 1 }, 37 | Pass 38 | >(), 39 | ]) 40 | 41 | checks([ 42 | // No effect if captures are absent 43 | check, {}, Pass>(), 44 | check, {}, Pass>(), 45 | check, {}, Pass>(), 46 | check, {}, Pass>(), 47 | check>, {}, Pass>(), 48 | check, {}, Pass>(), 49 | check, {}, Pass>(), 50 | check, {}, Pass>(), 51 | check, {}, Pass>(), 52 | check, {}, Pass>(), 53 | check, {}, Pass>(), 54 | check, {}, Pass>(), 55 | check, {}, Pass>(), 56 | // single capture, const 57 | check>, $a, Pass>(), 58 | check>, $a<__>, Pass>(), 59 | check>, $a<1>, Pass>(), 60 | // Nested captures 61 | check>>, $a<$b<1>> | $b<1>, Pass>(), 62 | check< 63 | RecursiveCollect<$a<{ b: $b<__>; c: $c<__> }>>, 64 | $c<__> | $b<__> | $a<{ b: $b<__>; c: $c<__> }>, 65 | Pass 66 | >(), 67 | ]) 68 | 69 | checks([ 70 | check, {}, Pass>(), 71 | check, any, Pass>(), 72 | check, unknown, Pass>(), 73 | check, number, Pass>(), 74 | check, 1, Pass>(), 75 | check, any, Pass>(), 76 | check, string, Pass>(), 77 | check, any, Pass>(), 78 | check>>>, any, Pass>(), 79 | check, { a: any }, Pass>(), 80 | check, { a: any }, Pass>(), 81 | check, { a: any }, Pass>(), 82 | check }>, { a: any }, Pass>(), 83 | ]) 84 | 85 | checks([ 86 | check, {}, Pass>(), 87 | check, { a: any }, Pass>(), 88 | check>, { a: any }, Pass>(), 89 | check>, { a: string }, Pass>(), 90 | check>, { a: {} }, Pass>(), 91 | check | $b<{}>>, { a: {}; b: {} }, Pass>(), 92 | check< 93 | VariableCapture<$a<{ b: $b<{}> }> | $b<{}>>, 94 | { a: { b: {} }; b: {} }, 95 | Pass 96 | >(), 97 | check>, { a: { b: any } }, Pass>(), 98 | ]) 99 | -------------------------------------------------------------------------------- /test/cases/conformance/when.ts: -------------------------------------------------------------------------------- 1 | import { __, V, when } from 'src' 2 | import { Test } from 'ts-toolbelt' 3 | 4 | type Pass = Test.Pass 5 | 6 | const { check, checks } = Test 7 | 8 | when(__, value => { 9 | checks([check()]) 10 | }) 11 | 12 | when(V('x'), value => { 13 | checks([check()]) 14 | }) 15 | 16 | when(V('x', __), value => { 17 | checks([check()]) 18 | }) 19 | 20 | when(V('x', __.string), value => { 21 | checks([check()]) 22 | }) 23 | 24 | when(V('x', __.number), value => { 25 | checks([check()]) 26 | }) 27 | 28 | when({ x: V('x') }, value => { 29 | checks([check()]) 30 | }) 31 | 32 | when(V('x', { a: V('y', { b: { c: V('z', { d: __ }) } }) }), value => { 33 | checks([ 34 | check< 35 | typeof value, 36 | { 37 | x: { a: { b: { c: { d: any } } } } 38 | y: { b: { c: { d: any } } } 39 | z: { d: any } 40 | }, 41 | Pass 42 | >(), 43 | ]) 44 | }) 45 | 46 | when([V('hd')], value => { 47 | checks([check()]) 48 | }) 49 | 50 | when([V('hd'), V('tail', __.tail)], value => { 51 | checks([check()]) 52 | }) 53 | -------------------------------------------------------------------------------- /test/pack/commonjs/index.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const { match, __, V } = require("@dkendal/match"); 3 | 4 | assert.strictEqual(typeof match, "function"); 5 | 6 | const result = match( 7 | { type: "ok", value: { type: "img", src: "https://example.com/" } }, 8 | [{ value: { type: "img", src: V("src", __.string) } }, ({ src }) => src] 9 | ); 10 | 11 | assert.strictEqual(result, "https://example.com/"); 12 | 13 | console.log("Test suite pass: test-pack-commonjs"); 14 | -------------------------------------------------------------------------------- /test/pack/commonjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-pack-commonjs", 3 | "type": "commonjs", 4 | "packageManager": "yarn@3.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@dkendal/match": "workspace:*" 8 | }, 9 | "scripts": { 10 | "test": "node ./index.js" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/pack/esm/index.js: -------------------------------------------------------------------------------- 1 | import { strictEqual } from "assert"; 2 | 3 | import { match, __, V } from "@dkendal/match"; 4 | 5 | const result = match( 6 | { type: "ok", value: { type: "img", src: "https://example.com/" } }, 7 | [{ value: { type: "img", src: V("src", __.string) } }, ({ src }) => src] 8 | ); 9 | 10 | strictEqual(result, "https://example.com/"); 11 | 12 | console.log("Test suite pass: test-pack-esm"); 13 | -------------------------------------------------------------------------------- /test/pack/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-pack-esm", 3 | "packageManager": "yarn@3.1.0", 4 | "type": "module", 5 | "private": true, 6 | "dependencies": { 7 | "@dkendal/match": "workspace:*" 8 | }, 9 | "scripts": { 10 | "test": "node --trace-warnings ./index.js" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/pack/ts/index.ts: -------------------------------------------------------------------------------- 1 | import { strictEqual } from "assert"; 2 | 3 | import { match, __, V } from "@dkendal/match"; 4 | 5 | const result = match( 6 | { type: "ok", value: { type: "img", src: "https://example.com/" } }, 7 | [{ value: { type: "img", src: V("src", __.string) } }, ({ src }) => src] 8 | ); 9 | 10 | strictEqual(result, "https://example.com/"); 11 | 12 | console.log("Test suite pass: test-pack-ts"); 13 | -------------------------------------------------------------------------------- /test/pack/ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-pack-ts", 3 | "packageManager": "yarn@3.1.0", 4 | "type": "commonjs", 5 | "private": true, 6 | "dependencies": { 7 | "@dkendal/match": "workspace:*" 8 | }, 9 | "scripts": { 10 | "build": "yarn tsc --build ./tsconfig.json --verbose", 11 | "test": "node --trace-warnings ./build/index.js" 12 | }, 13 | "devDependencies": { 14 | "@tsconfig/node16": "^1.0.2", 15 | "typescript": "^4.5.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/pack/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "build" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | 4 | "extends": "./tsconfig.node10", 5 | 6 | "compilerOptions": { 7 | "rootDir": ".", 8 | "outDir": "build", 9 | 10 | "types": [] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig-release.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./src/tsconfig-release-esm.json" }, 4 | { "path": "./src/tsconfig-release-cjs.json" } 5 | ], 6 | 7 | "files": [] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | 4 | "extends": "./tsconfig.node10", 5 | 6 | "references": [ 7 | { "path": "./src" }, 8 | { "path": "./perf" }, 9 | { "path": "./test/cases/conformance" } 10 | ], 11 | 12 | "files": [] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.node10.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Node 10", 4 | 5 | "compilerOptions": { 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "target": "es2018", 9 | 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | 15 | "composite": true 16 | } 17 | } 18 | --------------------------------------------------------------------------------