├── .babelrc ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── benchmarks ├── README.md └── perfBenchmarks.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── index.test.ts └── index.ts ├── tsconfig.bench.json ├── tsconfig.json └── tslint.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/typescript"], 3 | "plugins": ["@babel/plugin-transform-modules-commonjs"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Example Contributing Guidelines 2 | 3 | This is an example of GitHub's contributing guidelines file. Check out GitHub's [CONTRIBUTING.md help center article](https://help.github.com/articles/setting-guidelines-for-repository-contributors/) for more information. 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * **I'm submitting a ...** 2 | [ ] bug report 3 | [ ] feature request 4 | [ ] question about the decisions made in the repository 5 | [ ] question about how to use this project 6 | 7 | * **Summary** 8 | 9 | 10 | 11 | * **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.) 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 2 | 3 | 4 | 5 | * **What is the current behavior?** (You can also link to an open issue here) 6 | 7 | 8 | 9 | * **What is the new behavior (if this is a feature change)?** 10 | 11 | 12 | 13 | * **Other information**: 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | pkg 3 | src/**.js 4 | 5 | coverage 6 | *.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | tsconfig.json 4 | tsconfig.module.json 5 | tslint.json 6 | .travis.yml 7 | .github 8 | .prettierignore 9 | build/docs 10 | **/*.spec.* 11 | coverage 12 | .nyc_output 13 | *.log 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Debug Project", 8 | // we test in `build` to make cleanup fast and easy 9 | "cwd": "${workspaceFolder}/build", 10 | // Replace this with your project root. If there are multiple, you can 11 | // automatically run the currently visible file with: "program": ${file}" 12 | "program": "${workspaceFolder}/src/cli/cli.ts", 13 | // "args": ["--no-install"], 14 | "outFiles": ["${workspaceFolder}/build/main/**/*.js"], 15 | "skipFiles": [ 16 | "/**/*.js", 17 | "${workspaceFolder}/node_modules/**/*.js" 18 | ], 19 | "preLaunchTask": "npm: build", 20 | "stopOnEntry": true, 21 | "smartStep": true, 22 | "runtimeArgs": ["--nolazy"], 23 | "env": { 24 | "TYPESCRIPT_STARTER_REPO_URL": "${workspaceFolder}" 25 | }, 26 | "console": "externalTerminal" 27 | }, 28 | { 29 | /// Usage: set appropriate breakpoints in a *.spec.ts file, then open the 30 | // respective *.spec.js file to run this task. Once a breakpoint is hit, 31 | // the debugger will open the source *.spec.ts file for debugging. 32 | "type": "node", 33 | "request": "launch", 34 | "name": "Debug Visible Compiled Spec", 35 | "program": "${workspaceFolder}/node_modules/ava/profile.js", 36 | "args": [ 37 | "${file}" 38 | // TODO: VSCode's launch.json variable substitution 39 | // (https://code.visualstudio.com/docs/editor/variables-reference) 40 | // doesn't quite allow us to go from: 41 | // `./src/path/to/file.ts` to `./build/main/path/to/file.js` 42 | // so the user has to navigate to the compiled file manually. (Close:) 43 | // "${workspaceFolder}/build/main/lib/${fileBasenameNoExtension}.js" 44 | ], 45 | "skipFiles": ["/**/*.js"], 46 | // Consider using `npm run watch` or `yarn watch` for faster debugging 47 | // "preLaunchTask": "npm: build", 48 | // "smartStep": true, 49 | "runtimeArgs": ["--nolazy"] 50 | }] 51 | } 52 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.formatOnSave": true, 4 | "npm-scripts.showStartNotification": false 5 | // "typescript.implementationsCodeLens.enabled": true 6 | // "typescript.referencesCodeLens.enabled": true 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Simon 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 | # ts-union 2 | 3 | A tiny library for algebraic sum types in typescript. Inspired by [unionize](https://github.com/pelotom/unionize) and [F# discriminated-unions](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/discriminated-unions) (and other ML languages) 4 | 5 | ## Installation 6 | 7 | ``` 8 | npm add ts-union 9 | ``` 10 | 11 | NOTE: Distrubuted as modern javascript (es2018) library. 12 | 13 | ## Usage 14 | 15 | ### Define 16 | 17 | ```typescript 18 | import { Union, of } from 'ts-union'; 19 | 20 | const PaymentMethod = Union({ 21 | Check: of(), 22 | CreditCard: of(), 23 | Cash: of(null), // means that this variant has no payload 24 | }); 25 | 26 | type CheckNumber = number; 27 | type CardType = 'MasterCard' | 'Visa'; 28 | type CardNumber = string; 29 | ``` 30 | 31 | ### Construct a union value 32 | 33 | ```typescript 34 | // Check is a function that accepts a check number 35 | const check = PaymentMethod.Check(15566909); 36 | 37 | // CreditCard is a function that accepts two arguments (CardType, CardNumber) 38 | const card = PaymentMethod.CreditCard('Visa', '1111-566-...'); 39 | 40 | // Cash is just a value 41 | const cash = PaymentMethod.Cash; 42 | 43 | // or destructure it to simplify construction :) 44 | const { Cash, Check, CreditCard } = PaymentMethod; 45 | const anotherCheck = Check(566541123); 46 | ``` 47 | 48 | ### `match` 49 | 50 | ```typescript 51 | const str = PaymentMethod.match(cash, { 52 | Cash: () => 'cash', 53 | Check: (n) => `check num: ${n.toString()}`, 54 | CreditCard: (type, n) => `${type} ${n}`, 55 | }); 56 | ``` 57 | 58 | Also supports deferred (curried) matching and `default` case. 59 | 60 | ```typescript 61 | const toStr = PaymentMethod.match({ 62 | Cash: () => 'cash', 63 | default: (_v) => 'not cash', // _v is the union obj 64 | }); 65 | 66 | const str = toStr(card); // "not cash" 67 | ``` 68 | 69 | ### `if` (aka simplified match) 70 | 71 | ```typescript 72 | const str = PaymentMethod.if.Cash(cash, () => 'yep'); // "yep" 73 | // typeof str === string | undefined 74 | ``` 75 | 76 | You can provide else case as well, in that case 'undefined' type will be removed from the result. 77 | 78 | ```typescript 79 | // typeof str === string 80 | const str = PaymentMethod.if.Check( 81 | cash, 82 | (n) => `check num: ${n.toString()}`, 83 | (_v) => 'not check' // _v is the union obj that is passed in 84 | ); // str === 'not check' 85 | ``` 86 | 87 | ### **EXPERIMENTAL** `matchWith` 88 | 89 | WARNING: This API is experimental and currently more of an MVP. 90 | 91 | Often we want to match a union with another union. A good example of this if we try to model a state transition in `useReducer` in React or model a state machine. 92 | 93 | This is what you have to do currently: 94 | 95 | ```ts 96 | const State = Union({ 97 | Loading: of(null), 98 | Loaded: of(), 99 | Err: of(), 100 | }); 101 | 102 | const Ev = Union({ 103 | ErrorHappened: of(), 104 | DataFetched: of(), 105 | }); 106 | 107 | const { Loaded, Err, Loading } = State; 108 | 109 | const transition = (prev: typeof State.T, ev: typeof Ev.T) => 110 | State.match(prev, { 111 | Loading: () => 112 | Ev.match(ev, { 113 | ErrorHappened: (err) => Err(err), 114 | DataFetched: (data) => Loaded(data), 115 | }), 116 | 117 | Loaded: (loadedData) => 118 | // just add to the current loaded value as an example 119 | Ev.if.DataFetched( 120 | ev, 121 | (data) => Loaded(loadedData + data), 122 | () => prev 123 | ), 124 | 125 | default: (s) => s, 126 | }); 127 | ``` 128 | 129 | It gets worse and more verbose when complexity grows, also you have to match the `Ev` in each variant of `State`. 130 | 131 | In my experience this comes up often enough to justify a dedicated API for matching a pair: 132 | 133 | ```ts 134 | import { Union, of } from 'ts-union'; 135 | 136 | const State = Union({ 137 | Loading: of(null), 138 | Loaded: of(), 139 | Err: of(), 140 | }); 141 | 142 | const Ev = Union({ 143 | ErrorHappened: of(), 144 | DataFetched: of(), 145 | }); 146 | 147 | const { Loaded, Err, Loading } = State; 148 | 149 | const transition = State.matchWith(Ev, { 150 | Loading: { 151 | ErrorHappened: (_, err) => Err(err), 152 | DataFetched: (_, data) => Loaded(data), 153 | }, 154 | 155 | Loaded: { 156 | DataFetched: (loaded, data) => Loaded(loaded + data), 157 | }, 158 | 159 | default: (prevState, ev) => prevState, 160 | }); 161 | 162 | // usage 163 | const newState = transition(Loading, Ev.ErrorHappened('oops')); // <-- State.Err('oops') 164 | ``` 165 | 166 | `transition` is a function with type signature: (prev: State, ev: Ev) => State. 167 | Note that the return type is **inferred**, meaning that you can return whatever type you want :) 168 | 169 | ```ts 170 | const logLoadingTransition = State.matchWith(Ev, { 171 | Loading: { 172 | ErrorHappened: (_, err) => 'Oops, error happened: ' + err, 173 | DataFetched: (_, data) => 'Data loaded with: ' + data.toString(), 174 | }, 175 | default: () => '', 176 | }); 177 | ``` 178 | 179 | #### Caveats 180 | 181 | 1. Doesn't support generic version (yet?) 182 | 2. Doesn't work with unions that have more than 1 arguments in variants. E.g. `of()` will give an incomprehensible type error. 183 | 3. You cannot pass additional data to the update function. I'm tinkering about something like this for the future releases: 184 | 185 | ```ts 186 | const transition = State.matchWith(Ev, {...}, of()); 187 | transition = (prev, ev, someContextValue); 188 | ``` 189 | 190 | ### Two ways to specify variants with no payload 191 | 192 | You can define variants with no payload with either `of(null)` or `of()`; 193 | 194 | ```ts 195 | const Nope = Union({ 196 | Old: of(), // only option in 2.0 197 | New: of(null), // new syntax in 2.1 198 | }); 199 | 200 | // Note that New is a value not a function 201 | const nope = Nope.New; 202 | 203 | // here Old is a function 204 | const oldNope = Nope.Old(); 205 | ``` 206 | 207 | Note that `Old` will always allocate a new value while `New` **is** a value (thus more efficient). 208 | 209 | For generics the syntax differs a little bit: 210 | 211 | ```ts 212 | // generic version 213 | const Option = Union((t) => ({ 214 | None: of(null), 215 | Some: of(t), 216 | })); 217 | 218 | // we need to provide a type for the Option to "remember" it. 219 | const maybeNumber = Option.None(); 220 | ``` 221 | 222 | Even though `None` is a function, but it **always** returns the same value. It is just a syntax to "remember" the type it was constructed with; 223 | 224 | Speaking of generics... 225 | 226 | ### Generic version 227 | 228 | ```typescript 229 | // Pass a function that accepts a type token and returns a record 230 | const Maybe = Union((val) => ({ 231 | Nothing: of(null), // type is Of<[Unit]> 232 | Just: of(val), // type is Of<[Generic]> 233 | })); 234 | ``` 235 | 236 | Note that `val` is a **value** of the special type `Generic` that will be substituted with an actual type later on. It is just a variable name, pls feel free to name it whatever you feel like :) Maybe `a`, `T` or `TPayload`? 237 | 238 | This feature can be handy to model network requests (like in `Redux`): 239 | 240 | ```typescript 241 | const ReqResult = Union((data) => ({ 242 | Pending: of(null), 243 | Ok: of(data), 244 | Err: of(), 245 | })); 246 | 247 | // res is inferred as UnionValG 248 | const res = ReqResult.Ok('this is awesome!'); 249 | 250 | const status = ReqResult.match(res, { 251 | Pending: () => 'Thinking...', 252 | Err: (err) => 253 | typeof err === 'string' ? `Oops ${err}` : `Exception ${err.message}`, 254 | Ok: (str) => `Ok, ${str}`, 255 | }); // 'Ok, this is awesome!' 256 | ``` 257 | 258 | Let's try to build `map` and `bind` functions for `Maybe`: 259 | 260 | ```typescript 261 | const { Nothing, Just } = Maybe; 262 | 263 | // GenericValType is a helper that allows you to substitute Generic token type. 264 | type MaybeVal = GenericValType; 265 | 266 | const map = (val: MaybeVal, f: (a: A) => B) => 267 | Maybe.match(val, { 268 | Just: (v) => Just(f(v)), 269 | Nothing: () => Nothing(), // note that we have to explicitly provide B type here 270 | }); 271 | 272 | const bind = (val: MaybeVal, f: (a: A) => MaybeVal) => 273 | Maybe.if.Just( 274 | val, 275 | (a) => f(a), 276 | (n) => (n as unknown) as MaybeVal 277 | ); 278 | 279 | map(Just('a'), (s) => s.length); // -> Just(1) 280 | bind(Just(100), (n) => Just(n.toString())); // -> Just('100') 281 | 282 | map(Nothing(), (s) => s.length); // -> Nothing 283 | ``` 284 | 285 | And if you want to **extend** `Maybe` with these functions: 286 | 287 | ```typescript 288 | const TempMaybe = Union(val => ({ 289 | Nothing: of(), 290 | Just: of(val) 291 | })); 292 | 293 | const map = ..... 294 | const bind = ..... 295 | 296 | // TempMaybe is just an object, so this is perfectly legit 297 | export const Maybe = {...TempMaybe, map, bind}; 298 | ``` 299 | 300 | ### Type of resulted objects 301 | 302 | Types of union values are opaque. That makes it possible to experiment with different underlying data structures. 303 | 304 | ```typescript 305 | type CashType = typeof cash; 306 | // UnionVal<{Cash:..., Check:..., CreditCard:...}> 307 | // and it is the same for card and check 308 | ``` 309 | 310 | The `UnionVal<...>` type for `PaymentMethod` is accessible via phantom property `T` 311 | 312 | ```typescript 313 | type PaymentMethodType = typeof PaymentMethod.T; 314 | // UnionVal<{Cash:..., Check:..., CreditCard:...}> 315 | ``` 316 | 317 | ## API and implementation details 318 | 319 | If you log a union value to console you will see a plain object. 320 | 321 | ```typescript 322 | console.log(PaymentMethod.Check(15566909)); 323 | // {k:'Check', p0:15566909, p1: undefined, p2: undefined, a: 1} 324 | ``` 325 | 326 | This is because union values are objects under the hood. The `k` element is the key, `p0` - `p1` are passed in parameters and `a` is the number of parameters. I decided not to expose that through typings but I might reconsider that in the future. You **cannot** use it for redux actions, however you can **safely use it for redux state**. 327 | 328 | Note that in version 2.0 it was a tuple. But [benchmarks](https://github.com/twop/ts-union/tree/master/benchmarks) showed that object are more efficient (I have no idea why arrays cannot be jitted efficiently). You can find more details below 329 | 330 | ### API 331 | 332 | Use `Union` constructor to define the type 333 | 334 | ```typescript 335 | import { Union, of } from 'ts-union'; 336 | 337 | const U = Union({ 338 | Simple: of(), // or of(). no payload. 339 | SuperSimple: of(null), // static union value with no payload 340 | One: of(), // one argument 341 | Const: of(3), // one constant argument that is baked in 342 | Two: of(), // two arguments 343 | Three: of(), // three 344 | }); 345 | 346 | // generic version 347 | const Option = Union((t) => ({ 348 | None: of(null), 349 | Some: of(t), // Note: t is a value of the special type Generic 350 | })); 351 | 352 | // for static variant values you still have to provide a type 353 | // because it needs to "remember" the type. 354 | // Thus a function call, but it will always return the same object 355 | const opt = Option.None(); 356 | 357 | // But here type is inferred as number 358 | const opt2 = Option.Some(5); 359 | ``` 360 | 361 | Let's take a closer look at `of` function 362 | 363 | ```typescript 364 | export interface Types { 365 | (unit: null): Of<[Unit]>; 366 | (): Of<[T]>; 367 | (g: Generic): Of<[Generic]>; 368 | (val: T): Const; 369 | (): Of<[T1, T2]>; 370 | (): Of<[T1, T2, T3]>; 371 | } 372 | declare const of: Types; 373 | ``` 374 | 375 | the actual implementation is pretty simple: 376 | 377 | ```typescript 378 | export const of: Types = ((val: any) => val) as any; 379 | ``` 380 | 381 | We just capture the constant and don't really care about the rest. Typescript will guide us to provide proper number of args for each case. 382 | 383 | `match` accepts either a full set of props or a subset with a default case. 384 | 385 | ```typescript 386 | // typedef for match function. Note there is a curried version 387 | export type MatchFunc = { 388 | (cases: MatchCases): ( 389 | val: UnionVal 390 | ) => Result; 391 | (val: UnionVal, cases: MatchCases): Result; 392 | }; 393 | ``` 394 | 395 | `if` either accepts a function that will be invoked (with a match) and/or else case. 396 | 397 | ```typescript 398 | // typedef for if case for one argument. 399 | // Note it doesn't throw but can return undefined 400 | { 401 | (val: UnionVal, f: (a: A) => R): R | undefined; 402 | (val: UnionVal, f: (a: A) => R, els: (v: UnionVal) => R): R; 403 | } 404 | ``` 405 | 406 | `GenericValType` is a type that helps with generic union values. It just replaces `Generic` token type with provided `Type`. 407 | 408 | ```typescript 409 | type GenericValType = Val extends UnionValG 410 | ? UnionValG 411 | : never; 412 | 413 | // Example 414 | import { Union, of, GenericValType } from 'ts-union'; 415 | const Maybe = Union((t) => ({ Nothing: of(), Just: of(t) })); 416 | type MaybeVal = GenericValType; 417 | ``` 418 | 419 | That's the whole API. 420 | 421 | ### Benchmarks 422 | 423 | You can find a more details [here](https://github.com/twop/ts-union/tree/master/benchmarks). Both `unionize` and `ts-union` are 1.2x -2x (ish?) times slower than handwritten discriminated unions: aka `{tag: 'num', n: number} | {tag: 'str', s: string}`. But the good news is that you don't have to write the boilerplate yourself, _and_ it is still blazing fast! 424 | 425 | ### Breaking changes from 2.1.1 -> 2.2.0 426 | 427 | There should be no public breaking changes, but I changed the underlying data structure (again!? and again!?) to be `{k: string, p0: any, p1: any, p2: any, a: number}`, where k is a case name like `"CreditCard"`, `p0`-`p2` passed in parameters and `a` is how many parameters were passed in. So if you stored the values somewhere (localStorage?) then please migrate accordingly. 428 | 429 | ```ts 430 | const oldShape = { k: 'CreditCard', p: ['Visa', '1111-566-...'] }; 431 | const newShape = { 432 | k: 'CreditCard', 433 | p0: 'Visa', 434 | p1: '1111-566-...', 435 | p2: undefined, 436 | a: 2, 437 | }; 438 | ``` 439 | 440 | motivation for this is potential perf wins avoiding dealing with `(...args) => {...}`. The current approach should be more friendly for JIT compilers (arguments and ...args are hard to optimize). That kinda aligns with my local perf results: 441 | 442 | old shape 443 | 444 | ``` 445 | Creation 446 | baseline: 8.39 ms 447 | unionize: 17.32 ms 448 | ts-union: 11.10 ms 449 | 450 | Matching with inline object 451 | baseline: 1.97 ms 452 | unionize: 5.96 ms 453 | ts-union: 7.32 ms 454 | 455 | Matching with preallocated function 456 | baseline: 2.20 ms 457 | unionize: 4.21 ms 458 | ts-union: 4.52 ms 459 | 460 | Mapping 461 | baseline: 2.02 ms 462 | unionize: 2.98 ms 463 | ts-union: 1.69 ms 464 | ``` 465 | 466 | new shape 467 | 468 | ``` 469 | Creation 470 | baseline: 6.90 ms 471 | unionize: 15.62 ms 472 | ts-union: 6.38 ms 473 | 474 | Matching with inline object 475 | baseline: 2.33 ms 476 | unionize: 6.26 ms 477 | ts-union: 5.19 ms 478 | 479 | Matching with preallocated function 480 | baseline: 1.67 ms 481 | unionize: 4.44 ms 482 | ts-union: 3.88 ms 483 | 484 | Mapping 485 | baseline: 1.96 ms 486 | unionize: 2.93 ms 487 | ts-union: 1.39 ms 488 | ``` 489 | 490 | ### Breaking changes from 2.0.1 -> 2.1 491 | 492 | There should be no public breaking changes, but I changed the underlying data structure (again!?) to be `{k: string, p: any[]}`, where k is a case name like `"CreditCard"` and p is a payload array. So if you stored the values somewhere (localStorage?) then please migrate accordingly. 493 | 494 | The motivation for it that I finally tried to benchmark the performance of the library. Arrays were 1.5x - 2x slower than plain objects :( 495 | 496 | ```ts 497 | const oldShape = ['CreditCard', ['Visa', '1111-566-...']]; 498 | 499 | // and yes this is faster. Blame V8. 500 | const newShape = { k: 'CreditCard', p: ['Visa', '1111-566-...'] }; 501 | ``` 502 | 503 | ### Breaking changes from 1.2 -> 2.0 504 | 505 | There should be no breaking changes, but I completely rewrote the types that drive public api. So if you for some reasons used them pls look into d.ts file for a replacement. 506 | 507 | ### Breaking changes from 1.1 -> 1.2 508 | 509 | - `t` function to define shapes is renamed to `of`. 510 | - There is a different underlying data structure. So if you persisted the values somewhere it wouldn't be compatible with the new version. 511 | 512 | The actual change is pretty simple: 513 | 514 | ```typescript 515 | type OldShape = [string, ...payload[any]]; 516 | // Note: no nesting 517 | const oldShape = ['CreditCard', 'Visa', '1111-566-...']; 518 | 519 | type NewShape = [string, payload[any]]; 520 | // Note: captured payload is nested 521 | const newShape = ['CreditCard', ['Visa', '1111-566-...']]; 522 | ``` 523 | 524 | That reduces allocations and opens up possibility for future API extensions. Such as: 525 | 526 | ```typescript 527 | // namespaces to avoid collisions. 528 | const withNamespace = ['CreditCard', ['Visa', '1111-566-...'], 'MyNamespace']; 529 | ``` 530 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Just primitive benchmarks 2 | 3 | Currently we compare three ADT approaches that provide similar functionality. 4 | 5 | - ts-union (this library) 6 | - [unionize](https://github.com/pelotom/unionize) 7 | - baseline - manual approach via `{tag:'tag1'...}` powered by ts control flow analysis 8 | 9 | ```ts 10 | // ts-union 11 | const U = Union({ 12 | Num: of(), 13 | Str: of(), 14 | None: of(null), 15 | Two: of() 16 | }); 17 | 18 | // unionize 19 | const Un = unionize( 20 | { 21 | Num: ofType(), 22 | Str: ofType(), 23 | Two: ofType<{ n: number; b: boolean }>(), 24 | None: {} 25 | }, 26 | { value: 'value' } 27 | ); 28 | 29 | // baseline 30 | type UB = 31 | | { tag: 'Num'; n: number } 32 | | { tag: 'Str'; s: string } 33 | | { tag: 'None' } 34 | | { tag: 'Two'; n: number; b: boolean }; 35 | ``` 36 | 37 | There are 4 benchmark categories: 38 | 39 | - Creation of a union value 40 | - Match union value to produce a string via inline object 41 | - Match union value to produce a string via cached functions 42 | - Map `Num` union value to produce a `Str` value out of it 43 | 44 | If you just want to see the numbers then scroll to the bottom :) 45 | 46 | ## Creation 47 | 48 | All of them need to provide constructors from a number. The idea is that we preallocate an array for all of them (to avoid any GC or array resizing), and then with equal probability invoke one of the constructors. 49 | 50 | Note currently the number of elements is 2000000. 51 | 52 | ### baseline 53 | 54 | ```ts 55 | const cachedNone: UB = { tag: 'None' }; 56 | const Num = (n: number): UB => ({ tag: 'Num', n }); 57 | const Str = (s: string): UB => ({ tag: 'Str', s }); 58 | const None = (): UB => cachedNone; 59 | const Two = (n: number, b: boolean): UB => ({ tag: 'Two', n, b }); 60 | 61 | const cases = [ 62 | Num, 63 | (i: number) => Str(i.toString()), 64 | (i: number) => Two(i, i % 2 === 0), 65 | () => None() 66 | ]; 67 | ``` 68 | 69 | ### unionize 70 | 71 | ```ts 72 | const cases = [ 73 | Un.Num, 74 | (i: number) => Un.Str(i.toString()), 75 | (i: number) => Un.Two({ n: i, b: i % 2 === 0 }), 76 | () => Un.None() 77 | ]; 78 | ``` 79 | 80 | ### ts-union 81 | 82 | ```ts 83 | // almost identical to unionize 84 | const cases = [ 85 | U.Num, 86 | (i: number) => U.Str(i.toString()), 87 | (i: number) => U.Two(i, i % 2 === 0), 88 | () => U.None 89 | ]; 90 | ``` 91 | 92 | ## Matching with inline cases object 93 | 94 | Both unionize ad ts-union provide an api to extract a value with an object that has match cases. In this benchmark we need to produce a string value out of a union object. 95 | 96 | ### baseline 97 | 98 | There is no such concept for baseline. So there is going to be just a manual `switch` statement. 99 | 100 | ```ts 101 | const baselineToString = (v: UB): string => { 102 | switch (v.tag) { 103 | case 'None': 104 | return 'none'; 105 | case 'Num': 106 | return v.n.toString(); 107 | case 'Str': 108 | return v.s; 109 | case 'Two': 110 | return v.n.toString() + v.b; 111 | } 112 | }; 113 | ``` 114 | 115 | ### unionize 116 | 117 | ```ts 118 | // note that the cases object is constructed with each call 119 | const toStr = (v: UnT) => 120 | Un.match(v, { 121 | Num: n => n.toString(), 122 | Str: s => s, 123 | None: () => 'none', 124 | Two: two => two.n.toString() + two.b 125 | }); 126 | ``` 127 | 128 | ### ts-union 129 | 130 | ```ts 131 | // again, almost identical to unionize 132 | const toStr = (v: UT) => 133 | U.match(v, { 134 | Num: n => n.toString(), 135 | Str: s => s, 136 | None: () => 'none', 137 | Two: (n, b) => n.toString() + b 138 | }); 139 | ``` 140 | 141 | ## Matching with cached cases function 142 | 143 | Both unionize ad ts-union allow to build a function out of cases object. 144 | 145 | ### baseline 146 | 147 | No changes for baseline 148 | 149 | ### unionize 150 | 151 | ```ts 152 | const toStr = Un.match({ 153 | Num: n => n.toString(), 154 | Str: s => s, 155 | None: () => 'none', 156 | Two: two => two.n.toString() + two.b 157 | }); 158 | ``` 159 | 160 | ### ts-union 161 | 162 | ```ts 163 | const toStr = U.match({ 164 | Num: n => n.toString(), 165 | Str: s => s, 166 | None: () => 'none', 167 | Two: (n, b) => n.toString() + b 168 | }); 169 | ``` 170 | 171 | ## Mapping 172 | 173 | The goal of this benchmark is to identify a `Num` case and convert it to `Str` case by simply calling `(n:number)=>n.toString()` 174 | 175 | ### baseline 176 | 177 | ```ts 178 | const numToStr = (v: UB): UB => { 179 | if (v.tag === 'Num') { 180 | return Str(v.n.toString()); 181 | } 182 | return v; 183 | }; 184 | ``` 185 | 186 | ### unionize 187 | 188 | ```ts 189 | // pretty cool transform api :) 190 | const numToStr = Un.transform({ Num: n => Un.Str(n.toString()) }); 191 | ``` 192 | 193 | ### ts-union 194 | 195 | ```ts 196 | // hoist cases handlers 197 | const identity = (t: T) => t; 198 | const n2s = (n: number) => U.Str(n.toString()); 199 | const numToStr = (v: UT) => U.if.Num(v, n2s, identity); 200 | ``` 201 | 202 | ## Results 203 | 204 | The benchmarks are performed this way: 205 | 206 | - run 50 times 207 | - take 5 fastest 208 | - return average of them 209 | 210 | I have 6 cores I7 mac mini 2018 model. 211 | 212 | ``` 213 | Testing: 1000000 elements 214 | 215 | Creation 216 | baseline: 50.18 ms 217 | unionize: 159.61 ms 218 | ts-union: 183.96 ms 219 | 220 | Matching with inline object 221 | baseline: 58.30 ms 222 | unionize: 126.50 ms 223 | ts-union: 130.10 ms 224 | 225 | Matching with preallocated function 226 | baseline: 57.37 ms 227 | unionize: 78.53 ms 228 | ts-union: 83.43 ms 229 | 230 | Mapping 231 | baseline: 9.28 ms 232 | unionize: 21.52 ms 233 | ts-union: 11.78 ms 234 | ``` 235 | 236 | ## Conclusion 237 | 238 | Nothing beats handwritten switch case :) But both `unionize` and `ts-union` provided a comparable performance with baseline when a matching function is cached. 239 | 240 | Note that I get different results all the time even with 50 attempts. So think about these numbers as a very rough approximation. In the real world usecases these functions might not even be considered as "hot". Thus, in my opinion the performance of these libraries is "good enough" to use them without thinking too much about it. 241 | -------------------------------------------------------------------------------- /benchmarks/perfBenchmarks.ts: -------------------------------------------------------------------------------- 1 | import { ofType, unionize } from 'unionize'; 2 | import { of, Union } from '../src/index'; 3 | 4 | const U = Union({ 5 | Num: of(), 6 | Str: of(), 7 | None: of(null), 8 | Two: of() 9 | }); 10 | 11 | const Un = unionize( 12 | { 13 | Num: ofType(), 14 | Str: ofType(), 15 | Two: ofType<{ n: number; b: boolean }>(), 16 | None: {} 17 | }, 18 | { value: 'value' } 19 | ); 20 | 21 | type UB = 22 | | { tag: 'Num'; n: number } 23 | | { tag: 'Str'; s: string } 24 | | { tag: 'None' } 25 | | { tag: 'Two'; n: number; b: boolean }; 26 | 27 | const cachedNone: UB = { tag: 'None' }; 28 | const Num = (n: number): UB => ({ tag: 'Num', n }); 29 | const Str = (s: string): UB => ({ tag: 'Str', s }); 30 | const None = (): UB => cachedNone; 31 | const Two = (n: number, b: boolean): UB => ({ tag: 'Two', n, b }); 32 | 33 | type UT = typeof U.T; 34 | type UnT = typeof Un._Union; 35 | 36 | type CreateCase = (i: number) => T; 37 | type Creation = { 38 | name: string; 39 | cases: [CreateCase, CreateCase, CreateCase, CreateCase]; 40 | }; 41 | 42 | type Matching = { 43 | name: string; 44 | toNum: (t: T) => number; 45 | }; 46 | 47 | type Mapping = { 48 | name: string; 49 | map: (t: T) => T; 50 | }; 51 | 52 | type Scenario = [Creation, Matching, Matching, Mapping]; 53 | 54 | const COUNT = 500000; 55 | const log = console.log; 56 | 57 | const measure = (name: string, func: () => void) => { 58 | //log(`\n${' '.repeat(4)}${name}`); 59 | 60 | // let fastest = 100500; 61 | 62 | const numOfRuns = 2; 63 | const takeTop = 1; 64 | 65 | let runs: number[] = []; 66 | for (let i = 0; i < numOfRuns; i++) { 67 | const hrstart = process.hrtime(); 68 | func(); 69 | const hrend = process.hrtime(hrstart); 70 | 71 | const current = hrend[1] / 1000000; 72 | 73 | runs.push(current); 74 | 75 | // fastest = Math.min(fastest, current); 76 | } 77 | 78 | const result = 79 | runs 80 | .sort((a, b) => a - b) 81 | .slice(0, takeTop) 82 | .reduce((s, v) => s + v, 0) / 5; 83 | 84 | log(`${' '.repeat(4)}${name}: ${result.toFixed(2)} ms`); 85 | }; 86 | 87 | const run = (scenarios: Scenario[]) => { 88 | const results: number[] = Array.from({ length: COUNT }, () => 0); 89 | const numbers: number[] = Array.from({ length: COUNT }, () => Math.random()); 90 | 91 | const values = scenarios.map(_ => 92 | Array.from({ length: COUNT }, () => ({} as any)) 93 | ); 94 | 95 | log(`Testing: ${COUNT} elements`); 96 | 97 | log(`\nCreation`); 98 | scenarios 99 | .map(s => s[0]) 100 | .forEach(({ name, cases }, sIndex) => 101 | measure(name, () => { 102 | for (let i = 0; i < COUNT; i++) { 103 | const seed = numbers[i]; 104 | const index = seed < 0.25 ? 0 : seed < 0.5 ? 1 : seed < 0.75 ? 2 : 3; 105 | values[sIndex][i] = cases[index](i); 106 | } 107 | }) 108 | ); 109 | 110 | log(`\nMatching with inline object`); 111 | scenarios 112 | .map(s => s[1]) 113 | .forEach(({ name, toNum: toStr }, sIndex) => 114 | measure(name, () => { 115 | for (let i = 0; i < COUNT; i++) { 116 | results[i] = toStr(values[sIndex][i]); 117 | } 118 | }) 119 | ); 120 | 121 | log(`\nMatching with preallocated function`); 122 | scenarios 123 | .map(s => s[2]) 124 | .forEach(({ name, toNum: toStr }, sIndex) => 125 | measure(name, () => { 126 | for (let i = 0; i < COUNT; i++) { 127 | results[i] = toStr(values[sIndex][i]); 128 | } 129 | }) 130 | ); 131 | 132 | log(`\nMapping`); 133 | scenarios 134 | .map(s => s[3]) 135 | .forEach(({ name, map }, sIndex) => 136 | measure(name, () => { 137 | const curValues = values[sIndex]; 138 | for (let i = 0; i < COUNT; i++) { 139 | curValues[i] = map(curValues[i]); 140 | } 141 | }) 142 | ); 143 | }; 144 | 145 | const tsUnionName = 'ts-union'; 146 | const tsUnionScenario: Scenario = [ 147 | { 148 | name: tsUnionName, 149 | cases: [ 150 | U.Num, 151 | (i: number) => U.Str(i.toString()), 152 | (i: number) => U.Two(i, i % 2 === 0), 153 | () => U.None 154 | ] 155 | }, 156 | { 157 | name: tsUnionName, 158 | toNum: v => 159 | U.match(v, { 160 | Num: n => n, 161 | Str: s => s.length, 162 | None: () => -100500, 163 | Two: (n, b) => n + (b ? 1 : -1) 164 | }) 165 | }, 166 | { 167 | name: tsUnionName, 168 | toNum: U.match({ 169 | Num: n => n, 170 | Str: s => s.length, 171 | None: () => -1004, 172 | Two: (n, b) => n + (b ? 1 : -1) 173 | }) 174 | }, 175 | { 176 | name: tsUnionName, 177 | map: (() => { 178 | const identity = (t: T) => t; 179 | const numToStr = (n: number) => U.Str(n.toString()); 180 | return (v: UT) => U.if.Num(v, numToStr, identity); 181 | })() 182 | } 183 | ]; 184 | 185 | const unionizeName = 'unionize'; 186 | const unionizeScenario: Scenario = [ 187 | { 188 | name: unionizeName, 189 | cases: [ 190 | Un.Num, 191 | (i: number) => Un.Str(i.toString()), 192 | (i: number) => Un.Two({ n: i, b: i % 2 === 0 }), 193 | () => Un.None() 194 | ] 195 | }, 196 | { 197 | name: unionizeName, 198 | toNum: v => 199 | Un.match(v, { 200 | Num: n => n, 201 | Str: s => s.length, 202 | None: () => -100500, 203 | Two: two => two.n + (two.b ? 1 : -1) 204 | }) 205 | }, 206 | { 207 | name: unionizeName, 208 | toNum: Un.match({ 209 | Num: n => n, 210 | Str: s => s.length, 211 | None: () => -100500, 212 | Two: ({ n, b }) => n + (b ? 1 : -1) 213 | }) 214 | }, 215 | { 216 | name: unionizeName, 217 | map: Un.transform({ Num: n => Un.Str(n.toString()) }) 218 | } 219 | ]; 220 | 221 | const baselineToNumber = (v: UB): number => { 222 | switch (v.tag) { 223 | case 'None': 224 | return -100500; 225 | case 'Num': 226 | return v.n; 227 | case 'Str': 228 | return v.s.length; 229 | case 'Two': 230 | return v.n + (v.b ? 1 : -1); 231 | } 232 | }; 233 | 234 | const baselineName = 'baseline'; 235 | const baselineScenario: Scenario = [ 236 | { 237 | name: baselineName, 238 | cases: [ 239 | Num, 240 | (i: number) => Str(i.toString()), 241 | (i: number) => Two(i, i % 2 === 0), 242 | () => None() 243 | ] 244 | }, 245 | { 246 | name: baselineName, 247 | toNum: baselineToNumber 248 | }, 249 | { 250 | name: baselineName, 251 | toNum: baselineToNumber 252 | }, 253 | { 254 | name: baselineName, 255 | map: (v: UB): UB => { 256 | if (v.tag === 'Num') { 257 | return Str(v.n.toString()); 258 | } 259 | return v; 260 | } 261 | } 262 | ]; 263 | 264 | run([baselineScenario, unionizeScenario, tsUnionScenario]); 265 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/dg/f15wxbj936756wddvvd74v_00000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: 'coverage', 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files usin a array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: [ 65 | // "node_modules" 66 | // ], 67 | 68 | // An array of file extensions your modules use 69 | // moduleFileExtensions: [ 70 | // "js", 71 | // "json", 72 | // "jsx", 73 | // "ts", 74 | // "tsx", 75 | // "node" 76 | // ], 77 | 78 | // A map from regular expressions to module names that allow to stub out resources with a single module 79 | // moduleNameMapper: {}, 80 | 81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 82 | // modulePathIgnorePatterns: [], 83 | 84 | // Activates notifications for test results 85 | // notify: false, 86 | 87 | // An enum that specifies notification mode. Requires { notify: true } 88 | // notifyMode: "failure-change", 89 | 90 | // A preset that is used as a base for Jest's configuration 91 | // preset: '@babel/typescript', 92 | 93 | // Run tests from one or more projects 94 | // projects: null, 95 | 96 | // Use this configuration option to add custom reporters to Jest 97 | // reporters: undefined, 98 | 99 | // Automatically reset mock state between every test 100 | // resetMocks: false, 101 | 102 | // Reset the module registry before running each individual test 103 | // resetModules: false, 104 | 105 | // A path to a custom resolver 106 | // resolver: null, 107 | 108 | // Automatically restore mock state between every test 109 | // restoreMocks: false, 110 | 111 | // The root directory that Jest should scan for tests and modules within 112 | // rootDir: null, 113 | 114 | // A list of paths to directories that Jest should use to search for files in 115 | // roots: [ 116 | // "" 117 | // ], 118 | 119 | // Allows you to use a custom runner instead of Jest's default test runner 120 | // runner: "jest-runner", 121 | 122 | // The paths to modules that run some code to configure or set up the testing environment before each test 123 | // setupFiles: [], 124 | 125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 126 | // setupFilesAfterEnv: [], 127 | 128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 129 | // snapshotSerializers: [], 130 | 131 | // The test environment that will be used for testing 132 | testEnvironment: 'node', 133 | 134 | // Options that will be passed to the testEnvironment 135 | // testEnvironmentOptions: {}, 136 | 137 | // Adds a location field to test results 138 | // testLocationInResults: false, 139 | 140 | // The glob patterns Jest uses to detect test files 141 | testMatch: ['**/__tests__/**/*.[jt]s?(x)'] 142 | // '**/?(*.)+(spec|test).[tj]s?(x)'] 143 | 144 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 145 | // testPathIgnorePatterns: [ 146 | // "/node_modules/" 147 | // ], 148 | 149 | // The regexp pattern or array of patterns that Jest uses to detect test files 150 | // testRegex: [], 151 | 152 | // This option allows the use of a custom results processor 153 | // testResultsProcessor: null, 154 | 155 | // This option allows use of a custom test runner 156 | // testRunner: "jasmine2", 157 | 158 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 159 | // testURL: "http://localhost", 160 | 161 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 162 | // timers: "real", 163 | 164 | // A map from regular expressions to paths to transformers 165 | // transform: null, 166 | 167 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 168 | // transformIgnorePatterns: [ 169 | // "/node_modules/" 170 | // ], 171 | 172 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 173 | // unmockedModulePathPatterns: undefined, 174 | 175 | // Indicates whether each individual test should be reported during the run 176 | // verbose: null, 177 | 178 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 179 | // watchPathIgnorePatterns: [], 180 | 181 | // Whether to use watchman for file crawling 182 | // watchman: true, 183 | }; 184 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-union", 3 | "version": "2.3.0", 4 | "description": "ADT (sum type) in typescript inspired by ML language family", 5 | "repository": "https://github.com/twop/ts-union", 6 | "license": "MIT", 7 | "type": "module", 8 | "keywords": [ 9 | "adt", 10 | "typescript", 11 | "sum-types" 12 | ], 13 | "scripts": { 14 | "test": "ava -v", 15 | "pika-pack": "pika build", 16 | "pika-publish": "pika publish", 17 | "typecheck": "tsc --noEmit", 18 | "benchmark": "ts-node --project ./tsconfig.bench.json benchmarks/perfBenchmarks.ts", 19 | "version": "npx @pika/pack" 20 | }, 21 | "dependencies": {}, 22 | "devDependencies": { 23 | "@pika/pack": "^0.5.0", 24 | "@pika/plugin-build-node": "^0.9.2", 25 | "@pika/plugin-build-web": "^0.9.2", 26 | "@pika/plugin-ts-standard-pkg": "^0.9.2", 27 | "@types/node": "^14.0.11", 28 | "ava": "^3.13.0", 29 | "prettier": "^2.1.2", 30 | "ts-node": "^9.0.0", 31 | "typescript": "4.0.3", 32 | "unionize": "3.1.0" 33 | }, 34 | "prettier": { 35 | "arrowParens": "avoid", 36 | "printWidth": 100, 37 | "trailingComma": "es5", 38 | "singleQuote": true 39 | }, 40 | "ava": { 41 | "extensions": { 42 | "ts": "module" 43 | }, 44 | "nonSemVerExperiments": { 45 | "configurableModuleFormat": true 46 | }, 47 | "nodeArguments": [ 48 | "--loader=ts-node/esm", 49 | "--experimental-specifier-resolution=node" 50 | ], 51 | "files": [ 52 | "src/**/*.test.ts" 53 | ] 54 | }, 55 | "@pika/pack": { 56 | "pipeline": [ 57 | [ 58 | "@pika/plugin-ts-standard-pkg", 59 | { 60 | "exclude": [ 61 | "__tests__/**/*.*", 62 | "benchmarks/**/*.*" 63 | ] 64 | } 65 | ], 66 | [ 67 | "@pika/plugin-build-node" 68 | ], 69 | [ 70 | "@pika/plugin-build-web" 71 | ] 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-expression-statement 2 | // tslint:disable-next-line:no-implicit-dependencies 3 | import { GenericValType, of, Union } from './index'; 4 | 5 | import test from 'ava'; 6 | 7 | // tslint:disable-next-line:no-object-literal-type-assertion 8 | const U = Union({ 9 | Simple: of(), 10 | SuperSimple: of(null), 11 | One: of(), 12 | Const: of(3), 13 | Two: of(), 14 | Three: of(), 15 | }); 16 | 17 | const { SuperSimple, Simple, One, Two, Three, Const } = U; 18 | 19 | test('unpacks simple', t => { 20 | const s = Simple(); 21 | const c = Const(); 22 | 23 | t.is( 24 | U.if.Simple(s, () => 4), 25 | 4 26 | ); 27 | 28 | t.is( 29 | U.if.Simple(c, () => 4), 30 | undefined 31 | ); 32 | 33 | t.is( 34 | U.if.Simple( 35 | c, 36 | () => 4, 37 | () => 1 38 | ), 39 | 1 40 | ); 41 | }); 42 | 43 | test('unpacks const', t => { 44 | const si = Simple(); 45 | const c = Const(); 46 | t.is( 47 | U.if.Const(c, n => n), 48 | 3 49 | ); 50 | t.is( 51 | U.if.Const( 52 | si, 53 | n => n, 54 | () => 1 55 | ), 56 | 1 57 | ); 58 | t.is( 59 | U.if.Const(si, n => n), 60 | undefined 61 | ); 62 | }); 63 | 64 | test('else case accepts the original object', t => { 65 | const simple = Simple(); 66 | t.is( 67 | U.if.Const( 68 | simple, 69 | _ => Simple(), 70 | v => v 71 | ), 72 | simple 73 | ); 74 | }); 75 | 76 | test('unpacks one arg', t => { 77 | const one = One('one'); 78 | const c = Const(); 79 | t.is( 80 | U.if.One(one, s => s), 81 | 'one' 82 | ); 83 | t.is( 84 | U.if.One( 85 | c, 86 | s => s, 87 | _ => 'els' 88 | ), 89 | 'els' 90 | ); 91 | t.is( 92 | U.if.One(c, s => s), 93 | undefined 94 | ); 95 | }); 96 | 97 | test('unpacks two args', t => { 98 | const f = (s: string, n: number) => s + n.toString(); 99 | const two = Two('two', 1); 100 | const c = Const(); 101 | t.is(U.if.Two(two, f), 'two1'); 102 | t.is( 103 | U.if.Two(c, f, () => 'els'), 104 | 'els' 105 | ); 106 | t.is(U.if.Two(c, f), undefined); 107 | }); 108 | 109 | test('unpacks three args', t => { 110 | const f = (s: string, n: number, b: boolean) => s + n.toString() + (b ? 'true' : 'false'); 111 | const three = Three('three', 1, true); 112 | const c = Const(); 113 | t.is(U.if.Three(three, f), 'three1true'); 114 | t.is( 115 | U.if.Three(c, f, () => 'els'), 116 | 'els' 117 | ); 118 | t.is(U.if.Three(c, f), undefined); 119 | }); 120 | 121 | const throwErr = (): never => { 122 | throw new Error('shouldnt happen'); 123 | }; 124 | 125 | test('switch simple case', t => { 126 | t.is( 127 | U.match(Simple(), { 128 | Simple: () => 'simple', 129 | default: throwErr, 130 | }), 131 | 'simple' 132 | ); 133 | }); 134 | 135 | test('switch const case', t => { 136 | t.is( 137 | U.match(Const(), { 138 | Const: n => n, 139 | default: throwErr, 140 | }), 141 | 3 142 | ); 143 | }); 144 | 145 | test('switch one case', t => { 146 | t.is( 147 | U.match(One('one'), { 148 | One: s => s, 149 | default: throwErr, 150 | }), 151 | 'one' 152 | ); 153 | }); 154 | 155 | test('switch two case', t => { 156 | t.is( 157 | U.match(Two('two', 2), { 158 | Two: (s, n) => s + n.toString(), 159 | default: throwErr, 160 | }), 161 | 'two2' 162 | ); 163 | }); 164 | 165 | test('switch three case', t => { 166 | t.is( 167 | U.match(Three('three', 1, true), { 168 | Three: (s, n, b) => s + n.toString() + (b ? 'true' : 'false'), 169 | default: throwErr, 170 | }), 171 | 'three1true' 172 | ); 173 | }); 174 | 175 | test('switch deferred eval', t => { 176 | const evalFunc = U.match({ 177 | Simple: () => 'simple', 178 | SuperSimple: () => 'super simple', 179 | Const: n => n.toString(), 180 | One: s => s, 181 | Three: (s, n, b) => s + n.toString() + (b ? 'true' : 'false'), 182 | Two: (s, n) => s + n.toString(), 183 | }); 184 | 185 | t.is(evalFunc(Simple()), 'simple'); 186 | t.is(evalFunc(SuperSimple), 'super simple'); 187 | t.is(evalFunc(Const()), '3'); 188 | t.is(evalFunc(One('one')), 'one'); 189 | t.is(evalFunc(Two('two', 2)), 'two2'); 190 | t.is(evalFunc(Three('three', 3, true)), 'three3true'); 191 | }); 192 | 193 | test('switch default case', t => { 194 | const three = Three('three', 3, true); 195 | const two = Two('two', 2); 196 | const one = One('one'); 197 | const si = Simple(); 198 | const co = Const(); 199 | 200 | const val = 'def'; 201 | const justDef = U.match({ default: _ => val }); 202 | 203 | t.is(justDef(si), val); 204 | t.is(justDef(co), val); 205 | t.is(justDef(one), val); 206 | t.is(justDef(two), val); 207 | t.is(justDef(three), val); 208 | 209 | t.is(U.match(si, { default: _ => val }), val); 210 | t.is(U.match(co, { default: _ => val }), val); 211 | t.is(U.match(one, { default: _ => val }), val); 212 | t.is(U.match(two, { default: _ => val }), val); 213 | t.is(U.match(three, { default: _ => val }), val); 214 | }); 215 | 216 | const Maybe = Union(a => ({ 217 | Nothing: of(null), 218 | Just: of(a), 219 | })); 220 | 221 | const { Nothing, Just } = Maybe; 222 | 223 | test('generic match', t => { 224 | const shouldBeTwo = Maybe.match(Just(1), { 225 | Just: n => n + 1, 226 | default: throwErr, 227 | }); 228 | 229 | t.is(shouldBeTwo, 2); 230 | 231 | const numToStr = Maybe.match({ 232 | Just: (n: number) => n.toString(), 233 | Nothing: () => 'nothing', 234 | }); 235 | 236 | t.is(numToStr(Just(1)), '1'); 237 | t.is(numToStr(Nothing()), 'nothing'); 238 | 239 | const strLen = Maybe.match({ 240 | Just: s => s.length, 241 | Nothing: () => -1, 242 | }); 243 | 244 | t.is(strLen(Just('a')), 1); 245 | t.is(strLen(Nothing()), -1); 246 | }); 247 | 248 | test('generic if', t => { 249 | const one = Just(1); 250 | const nothing = Nothing(); 251 | 252 | t.is( 253 | Maybe.if.Just(one, n => n + 1), 254 | 2 255 | ); 256 | t.is( 257 | Maybe.if.Just(nothing, n => n), 258 | undefined 259 | ); 260 | t.is( 261 | Maybe.if.Nothing(nothing, () => 1), 262 | 1 263 | ); 264 | }); 265 | 266 | test('if can write a generic func like map or bind', t => { 267 | type MaybeVal = GenericValType; 268 | 269 | const map = (val: MaybeVal, f: (a: A) => B) => 270 | Maybe.if.Just( 271 | val, 272 | v => Just(f(v)), 273 | n => (n as unknown) as MaybeVal 274 | ); 275 | 276 | const maybeOne = map(Just('a'), s => s.length); 277 | 278 | t.is( 279 | Maybe.if.Just(maybeOne, n => n + 1), 280 | 2 281 | ); 282 | 283 | const bind = (val: MaybeVal, f: (a: A) => MaybeVal) => 284 | Maybe.if.Just( 285 | val, 286 | a => f(a), 287 | _ => Nothing() 288 | ); 289 | 290 | t.is( 291 | Maybe.if.Just( 292 | bind(Just(1), n => Just(n.toString())), 293 | s => s 294 | ), 295 | '1' 296 | ); 297 | }); 298 | 299 | // Used for Readme.md example 300 | // const ReqResult = Union(TPayload => ({ 301 | // Pending: of(), 302 | // Ok: TPayload, 303 | // Err: of() 304 | // })); 305 | 306 | // const res = ReqResult.Ok('this is awesome!'); 307 | 308 | // const toStr = ReqResult.match(res, { 309 | // Pending: () => 'Thinking...', 310 | // Err: err => 311 | // typeof err === 'string' ? `Oops ${err}` : `Exception ${err.message}`, 312 | // Ok: str => `Ok, ${str}` 313 | // }); 314 | 315 | test('we can have boolean and union values for cases', t => { 316 | // related to https://github.com/Microsoft/TypeScript/issues/7294 317 | 318 | const T = Union({ 319 | Bool: of(), 320 | StrOrNum: of(), 321 | Enum: of<'yes' | 'no'>(), 322 | Void: of(null), 323 | }); 324 | 325 | const toStr = T.match({ 326 | Void: () => 'void', 327 | Bool: b => (b === true ? 'true' : b === false ? 'false' : throwErr()), 328 | Enum: s => s, 329 | StrOrNum: sn => (typeof sn === 'number' ? sn.toString() : sn), 330 | }); 331 | 332 | t.is(toStr(T.Void), 'void'); 333 | t.is(toStr(T.Bool(true)), 'true'); 334 | t.is(toStr(T.Enum('yes')), 'yes'); 335 | t.is(toStr(T.StrOrNum(1)), '1'); 336 | t.is(toStr(T.StrOrNum('F* yeah!')), 'F* yeah!'); 337 | 338 | t.is( 339 | T.if.Void(T.Void, () => 'void'), 340 | 'void' 341 | ); 342 | 343 | const G = Union(a => ({ 344 | Val: of(a), 345 | Nope: of<'nope' | 100500>(), 346 | })); 347 | 348 | type Guess = GenericValType; 349 | 350 | const valOr = (val: Guess, def: A) => 351 | G.if.Val( 352 | val, 353 | v => v, 354 | () => def 355 | ); 356 | 357 | const strOrNumVal = 'v' as string | number; 358 | 359 | t.is(valOr(G.Val(strOrNumVal), 4), 'v'); 360 | 361 | t.is(valOr(G.Val(1), -1), 1); 362 | t.is(valOr(G.Val(1), -1), 1); 363 | t.is(valOr(G.Nope('nope'), -1), -1); 364 | t.is(valOr(G.Nope(100500), -1), -1); 365 | }); 366 | 367 | test('shorthand for declaring cases momoizes the value in generics', t => { 368 | const G = Union(a => ({ 369 | Val: of(a), 370 | Nope: of(null), 371 | Void: of(), 372 | })); 373 | 374 | // not the same reference 375 | t.not(G.Val(1), G.Val(1)); 376 | t.not(G.Void(), G.Void()); 377 | 378 | // but 'of(null)' memoizes it 379 | t.is(G.Nope(), G.Nope()); 380 | }); 381 | 382 | test('it can correctly create matchWith function', t => { 383 | const State = Union({ 384 | Loading: of(null), 385 | Loaded: of(), 386 | Err: of(), 387 | }); 388 | 389 | const Ev = Union({ 390 | ErrorHappened: of(), 391 | DataFetched: of(), 392 | }); 393 | 394 | const { Loaded, Err, Loading } = State; 395 | 396 | const transition = State.matchWith(Ev, { 397 | Loading: { 398 | ErrorHappened: (_, err) => Err(err), 399 | DataFetched: (_, data) => Loaded(data), 400 | }, 401 | 402 | Loaded: { 403 | DataFetched: (loaded, data) => Loaded(loaded + data), 404 | }, 405 | 406 | default: (prevState, _ev) => prevState, 407 | }); 408 | 409 | const { ErrorHappened, DataFetched } = Ev; 410 | 411 | // declared cases 412 | t.deepEqual(transition(Loading, ErrorHappened('oops')), Err('oops')); 413 | t.deepEqual(transition(Loading, DataFetched(1)), Loaded(1)); 414 | t.deepEqual(transition(Loaded(1), DataFetched(1)), Loaded(2)); 415 | // fallback to default 416 | t.deepEqual(transition(Loaded(1), ErrorHappened('oops')), Loaded(1)); 417 | }); 418 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export interface Const { 2 | readonly _const: T; 3 | } 4 | 5 | export interface Generic { 6 | opaque: 'Generic is a token type that is going to be replaced with a real type'; 7 | } 8 | 9 | export interface Unit { 10 | opaque: 'opaque token for empty payload type'; 11 | } 12 | 13 | export type Case = Of | Const; 14 | 15 | export interface RecordDict { 16 | readonly [key: string]: Case; 17 | } 18 | 19 | export type SingleDataCase = Of<[Unit]> | Const | Of<[unknown]>; 20 | export interface SingleDataRecordDict { 21 | readonly [key: string]: SingleDataCase; 22 | } 23 | 24 | export interface ForbidDefault { 25 | default?: never; 26 | } 27 | 28 | export type ForbidReservedProps = { 29 | readonly if?: never; 30 | readonly match?: never; 31 | readonly matchWith?: never; 32 | readonly T?: never; 33 | } & ForbidDefault; 34 | 35 | export type RequiredRecordType = RecordDict & ForbidReservedProps; 36 | export type SingleDataRecordType = SingleDataRecordDict & ForbidReservedProps; 37 | 38 | export interface Of { 39 | _opaque: T; 40 | } 41 | 42 | export interface Types { 43 | (unit: null): Of<[Unit]>; 44 | (): Of<[T]>; 45 | (g: Generic): Of<[Generic]>; 46 | (val: T): Const; 47 | (): Of<[T1, T2]>; 48 | (): Of<[T1, T2, T3]>; 49 | } 50 | 51 | export const of: Types = ((val: any) => val) as any; 52 | 53 | // -------------------------------------------------------- 54 | export interface UnionVal { 55 | readonly _opaqueToken: Record; 56 | } 57 | export interface UnionValG { 58 | readonly _opaqueToken: Record; 59 | readonly _type: P; 60 | } 61 | 62 | export type GenericValType = Val extends UnionValG 63 | ? UnionValG 64 | : never; 65 | 66 | // -------------------------------------------------------- 67 | export type Constructors = { 68 | [T in keyof Record]: CreatorFunc>; 69 | }; 70 | 71 | export type ConstructorsG = { 72 | [K in keyof Record]: CreatorFuncG; 73 | }; 74 | 75 | // -------------------------------------------------------- 76 | export type Cases = { 77 | [T in keyof Record]: MatchCaseFunc; 78 | }; 79 | 80 | export type CasesG = { 81 | [K in keyof Record]: MatchCaseFuncG; 82 | }; 83 | 84 | // -------------------------------------------------------- 85 | export type CreatorFunc = K extends Of 86 | ? A extends [void] 87 | ? () => UVal 88 | : A extends [Unit] 89 | ? UVal 90 | : A extends any[] 91 | ? (...p: A) => UVal 92 | : never 93 | : K extends Const 94 | ? () => UVal 95 | : never; 96 | 97 | export type CreatorFuncG = K extends Of 98 | ? A extends [void] 99 | ?

() => UnionValG 100 | : A extends [Unit] 101 | ?

() => UnionValG 102 | : A extends [Generic] 103 | ?

(val: P) => UnionValG 104 | : A extends any[] 105 | ?

(...p: A) => UnionValG 106 | : never 107 | : K extends Const 108 | ?

() => UnionValG 109 | : never; 110 | 111 | // -------------------------------------------------------- 112 | export type MatchCaseFunc = K extends Of 113 | ? A extends [void] 114 | ? () => Res 115 | : A extends [Unit] 116 | ? () => Res 117 | : A extends any[] 118 | ? (...p: A) => Res 119 | : never 120 | : K extends Const 121 | ? (c: C) => Res 122 | : never; 123 | 124 | export type MatchCaseFuncG = K extends Of 125 | ? A extends [void] 126 | ? () => Res 127 | : A extends [Unit] 128 | ? () => Res 129 | : A extends [Generic] 130 | ? (val: P) => Res 131 | : A extends any[] 132 | ? (...p: A) => Res 133 | : never 134 | : K extends Const 135 | ? (c: C) => Res 136 | : never; 137 | 138 | // -------------------------------------------------------- 139 | export type MatchCases = 140 | | (Cases & ForbidDefault) 141 | | (Partial> & { 142 | default: (val: UnionVal) => Result; 143 | }); 144 | 145 | export type MatchCasesG = 146 | | (CasesG & ForbidDefault) 147 | | (Partial> & { 148 | default: (val: UnionValG) => Result; 149 | }); 150 | 151 | // -------------------------------------------------------- 152 | export interface MatchFunc { 153 | (cases: MatchCases): (val: UnionVal) => Result; 154 | (val: UnionVal, cases: MatchCases): Result; 155 | } 156 | export interface MatchFuncG { 157 | (cases: MatchCasesG): (val: UnionValG) => Result; 158 | (val: UnionValG, cases: MatchCasesG): Result; 159 | } 160 | 161 | // -------------------------------------------------------- 162 | export type UnpackFunc = K extends Of 163 | ? A extends [void] 164 | ? { 165 | (val: UnionVal, f: () => R): R | undefined; 166 | (val: UnionVal, f: () => R, els: (v: UnionVal) => R): R; 167 | } 168 | : A extends [Unit] 169 | ? { 170 | (val: UnionVal, f: () => R): R | undefined; 171 | (val: UnionVal, f: () => R, els: (v: UnionVal) => R): R; 172 | } 173 | : A extends any[] 174 | ? { 175 | (val: UnionVal, f: (...p: A) => R): R | undefined; 176 | (val: UnionVal, f: (...p: A) => R, els: (v: UnionVal) => R): R; 177 | } 178 | : never 179 | : K extends Const 180 | ? { 181 | (val: UnionVal, f: (с: С) => R): R | undefined; 182 | (val: UnionVal, f: (с: С) => R, els: (v: UnionVal) => R): R; 183 | } 184 | : never; 185 | 186 | export type UnpackFuncG = K extends Of 187 | ? A extends [void] 188 | ? { 189 | (val: UnionValG, f: () => R): R | undefined; 190 | (val: UnionValG, f: () => R, els: (v: UnionValG) => R): R; 191 | } 192 | : A extends [Unit] 193 | ? { 194 | (val: UnionValG, f: () => R): R | undefined; 195 | (val: UnionValG, f: () => R, els: (v: UnionValG) => R): R; 196 | } 197 | : A extends [Generic] 198 | ? { 199 | (val: UnionValG, f: (val: P) => R): R | undefined; 200 | (val: UnionValG, f: (val: P) => R, els: (v: UnionValG) => R): R; 201 | } 202 | : A extends any[] 203 | ? { 204 | (val: UnionValG, f: (...p: A) => R): R | undefined; 205 | (val: UnionValG, f: (...p: A) => R, els: (v: UnionValG) => R): R; 206 | } 207 | : never 208 | : K extends Const 209 | ? { 210 | (val: UnionValG, f: (с: С) => R): R | undefined; 211 | (val: UnionValG, f: (с: С) => R, els: (v: UnionValG) => R): R; 212 | } 213 | : never; 214 | 215 | // -------------------------------------------------------- 216 | export type Unpack = { [K in keyof Rec]: UnpackFunc }; 217 | export type UnpackG = { [K in keyof Rec]: UnpackFuncG }; 218 | 219 | // -------------------------------------------------------- 220 | type UnionDesc = { 221 | if: Unpack; 222 | T: UnionVal; 223 | match: MatchFunc; 224 | matchWith: Rec extends SingleDataRecordType 225 | ? ( 226 | other: UnionDesc, 227 | matchObj: MatchCasesForTwo 228 | ) => (a: UnionVal, b: UnionVal) => Result 229 | : never; 230 | } & Constructors; 231 | 232 | export type UnionObj = UnionDesc; 233 | 234 | // export type GenericUnionObj = { 235 | export type GenericUnionDesc = { 236 | match: MatchFuncG; 237 | if: UnpackG; 238 | T: UnionValG; 239 | } & ConstructorsG; 240 | 241 | // -------------------------------------------------------- 242 | 243 | // ------------- Match Two ------------- 244 | 245 | type TypeOfLeg = Leg extends Of 246 | ? A extends [void] | [Unit] 247 | ? void 248 | : A extends [infer Value] 249 | ? Value 250 | : A extends Const 251 | ? C 252 | : never 253 | : never; 254 | 255 | export type MatchCaseFuncTwo = (a: TypeOfLeg, b: TypeOfLeg) => Res; 256 | 257 | export type CasesTwo = { 258 | [KA in keyof RecordA]?: { 259 | [KB in keyof RecordB]?: MatchCaseFuncTwo; 260 | }; 261 | }; 262 | 263 | export type MatchCasesForTwo = CasesTwo & { 264 | default: (a: UnionVal, b: UnionVal) => Result; 265 | }; 266 | 267 | // -------------------------------------------------------- 268 | export interface UnionFunc { 269 | (record: R): UnionDesc; 270 | (ctor: (g: Generic) => R): GenericUnionDesc; 271 | } 272 | 273 | export const Union: UnionFunc = ( 274 | recOrFunc: ((g: Generic) => R) | R 275 | ) => { 276 | const record = 277 | typeof recOrFunc === 'function' ? recOrFunc((undefined as unknown) as Generic) : recOrFunc; 278 | 279 | // tslint:disable-next-line:prefer-object-spread 280 | return Object.assign( 281 | { 282 | if: createUnpack(record), 283 | match: (a: any, b?: any) => (b ? evalMatch(a, b) : (val: any) => evalMatch(val, a)), 284 | matchWith: ( 285 | _other: UnionDesc, // yep, it is only used to "remember" type 286 | matchObj: MatchCasesForTwo 287 | ) => createMatchTupleFunction(matchObj), 288 | }, 289 | createConstructors(record, typeof recOrFunc === 'function') 290 | ) as any; 291 | }; 292 | 293 | const createMatchTupleFunction = < 294 | A extends RequiredRecordType, 295 | B extends RequiredRecordType, 296 | Result 297 | >( 298 | matchObj: MatchCasesForTwo 299 | ) => { 300 | const { default: def } = matchObj; 301 | return function matchTuple(a: UnionVal, b: UnionVal): Result { 302 | const { p0: valA, k: keyA } = (a as unknown) as Value; 303 | const { p0: valB, k: KeyB } = (b as unknown) as Value; 304 | 305 | if (keyA in matchObj) { 306 | const inner = matchObj[keyA]; 307 | if (inner !== undefined && KeyB in inner) { 308 | const matchedFunction = inner[KeyB]; 309 | if (matchedFunction !== undefined) { 310 | return matchedFunction(valA, valB); 311 | } 312 | } 313 | } 314 | return def(a, b); 315 | }; 316 | }; 317 | 318 | const evalMatch = ( 319 | val: any, 320 | cases: MatchCases 321 | ): any => { 322 | // first elem is always the key 323 | const handler = cases[getKey(val)] as any; 324 | return handler ? invoke(val, handler) : cases.default && cases.default(val); 325 | }; 326 | 327 | const createConstructors = ( 328 | rec: Record, 329 | isGeneric: boolean 330 | ): Constructors => { 331 | const result: Partial> = {}; 332 | // tslint:disable-next-line: forin 333 | for (const key in rec) { 334 | result[key] = createCtor(key, rec, isGeneric); 335 | } 336 | return result as Constructors; 337 | }; 338 | 339 | const createCtor = ( 340 | key: K, 341 | rec: Record, 342 | isGeneric: boolean 343 | ): CreatorFunc> => { 344 | const val: Case = rec[key]; 345 | 346 | // it means that it was constructed with of(null) 347 | if (val === null) { 348 | const frozenVal = Object.freeze(makeValue(key, undefined, undefined, undefined)) as any; 349 | return isGeneric ? () => frozenVal : frozenVal; 350 | } 351 | 352 | // tslint:disable-next-line:no-if-statement 353 | if (val !== undefined) { 354 | const res = makeValue(key, val, undefined, undefined) as any; 355 | return ((() => res) as any) as any; 356 | } 357 | 358 | return ((p0: any, p1: any, p2: any) => makeValue(key, p0, p1, p2)) as any; 359 | }; 360 | 361 | const createUnpack = (rec: Record): Unpack => { 362 | const result: Partial> = {}; 363 | // tslint:disable-next-line:forin 364 | for (const key in rec) { 365 | result[key] = createUnpackFunc(key); 366 | } 367 | return result as Unpack; 368 | }; 369 | 370 | const createUnpackFunc = ( 371 | key: K 372 | ): UnpackFunc => 373 | ((val: any, f: (...args: any[]) => any, els?: (v: any) => any) => 374 | getKey(val) === key ? invoke(val, f) : els && els(val)) as any; 375 | 376 | const makeValue = (k: any, p0: any, p1: any, p2: any): Value => ({ 377 | k, 378 | p0, 379 | p1, 380 | p2, 381 | a: arity(p0, p1, p2), 382 | }); 383 | 384 | type Value = { 385 | k: string; 386 | p0: any; 387 | p1: any; 388 | p2: any; 389 | a: 0 | 1 | 2 | 3; 390 | }; 391 | 392 | const invoke = (val: Value, f: (...args: any[]) => any) => { 393 | switch (val.a) { 394 | case 0: 395 | return f(); 396 | case 1: 397 | return f(val.p0); 398 | case 2: 399 | return f(val.p0, val.p1); 400 | case 3: 401 | return f(val.p0, val.p1, val.p2); 402 | } 403 | }; 404 | 405 | const getKey = (val: Value) => val.k; 406 | // const getParams = (val: any) => val.p; 407 | 408 | const arity = (p0: any, p1: any, p2: any): 0 | 1 | 2 | 3 => 409 | p2 !== undefined ? 3 : p1 !== undefined ? 2 : p0 !== undefined ? 1 : 0; 410 | -------------------------------------------------------------------------------- /tsconfig.bench.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "module": "commonjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "rootDir": "src", 5 | "moduleResolution": "node", 6 | "module": "esnext", 7 | "inlineSourceMap": true, 8 | // "noEmit": true, 9 | 10 | "types": ["node"], 11 | 12 | "strict": true /* Enable all strict type-checking options. */, 13 | 14 | /* Strict Type-Checking Options */ 15 | // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 16 | // "strictNullChecks": true /* Enable strict null checks. */, 17 | // "strictFunctionTypes": true /* Enable strict checking of function types. */, 18 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 19 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 20 | // "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 21 | 22 | /* Additional Checks */ 23 | "noUnusedLocals": true /* Report errors on unused locals. */, 24 | "noUnusedParameters": true /* Report errors on unused parameters. */, 25 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 26 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 27 | /* Debugging Options */ 28 | "traceResolution": false /* Report module resolution log messages. */, 29 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 30 | "listFiles": false /* Print names of files part of the compilation. */, 31 | "pretty": true /* Stylize errors and messages using color and context. */ 32 | 33 | /* Experimental Options */ 34 | // "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 35 | // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, 36 | }, 37 | "include": ["src/**/*.ts"], 38 | "exclude": ["node_modules/**", "benchmarks/**/*.ts"], 39 | "compileOnSave": false 40 | } 41 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-config-prettier"], 3 | "rules": { 4 | "object-literal-sort-keys": false, 5 | "interface-name": false 6 | } 7 | } 8 | --------------------------------------------------------------------------------