UNPKG

@utx/result

Version:

A typescript implementation of Rust's Result and Option objects.

462 lines (337 loc) 12.1 kB
# @utx/result > Forked from [@vultix's work](https://github.com/vultix/ts-results) A typescript implementation of Rust's [Result](https://doc.rust-lang.org/std/result/) and [Option](https://doc.rust-lang.org/std/option/) objects. Brings compile-time error checking and optional values to typescript. ## Contents - [Installation](#installation) - [Example](#example) - [Result Example](#result-example) - [Option Example](#option-example) - [Usage](#usage) - [Creation](#creation) - [Type Safety](#type-safety) - [Unwrap](#unwrap) - [Expect](#expect) - [Map, MapErr](#map-and-maperr) - [Else](#else) - [UnwrapOr](#unwrapor) - [Empty](#empty) - [Combining Results](#combining-results) - [Result.all](#result-all) - [Result.any](#result-any) - [Usage with rxjs](#usage-with-rxjs) - [resultMap](#resultmap) - [resultMapErr](#resultmaperr) - [resultMapTo](#resultmapto) - [resultMapErrTo](#resultmapto) - [elseMap](#elsemap) - [elseMapTo](#elsemapto) - [resultSwitchMap, resultMergeMap](#resultswitchmap-and-resultmergemap) - [filterResultOk](#filterresultok) - [filterResultErr](#filterresulterr) ## Installation NPM ```bash npm install @utx/results ``` Yarn ```bash yarn add @utx/results ``` ## Example ### Result Example Convert this: ```typescript import { existsSync, readFileSync } from 'fs'; function readFile(path: string): string { if (existsSync(path)) { return readFileSync(path); } else { // Callers of readFile have no way of knowing the function can fail throw new Error('invalid path'); } } // This line may fail unexpectedly without warnings from typescript const text = readFile('test.txt'); ``` To this: ```typescript import { existsSync, readFileSync } from 'fs'; import { Ok, Err, Result } from '@utx/result'; function readFile(path: string): Result<string, 'invalid path'> { if (existsSync(path)) { return new Ok(readFileSync(path)); // new is optional here } else { return new Err('invalid path'); // new is optional here } } // Typescript now forces you to check whether you have a valid result at compile time. const result = readFile('test.txt'); if (result.ok) { // text contains the file's content const text = result.val; } else { // err equals 'invalid path' const err = result.val; } ``` ### Option Example Convert this: ```typescript declare function getLoggedInUsername(): string | undefined; declare function getImageURLForUsername(username: string): string | undefined; function getLoggedInImageURL(): string | undefined { const username = getLoggedInUsername(); if (!username) { return undefined; } return getImageURLForUsername(username); } const stringUrl = getLoggedInImageURL(); const optionalUrl = stringUrl ? new URL(stringUrl) : undefined; console.log(optionalUrl); ``` To this: ```typescript import { Option, Some, None } from '@utx/result'; declare function getLoggedInUsername(): Option<string>; declare function getImageForUsername(username: string): Option<string>; function getLoggedInImage(): Option<string> { return getLoggedInUsername().andThen(getImageForUsername); } const optionalUrl = getLoggedInImage().map((url) => new URL(stringUrl)); console.log(optionalUrl); // Some(URL('...')) // To extract the value, do this: if (optionalUrl.some) { const url: URL = optionalUrl.val; } ``` ## Usage ```typescript import { Result, Err, Ok } from '@utx/result'; ``` #### Creation ```typescript let okResult: Result<number, Error> = Ok(10); let errorResult: Result<number, Error> = Err(new Error('bad number!')); ``` #### Type Safety _Note: Typescript currently has a [bug](https://github.com/microsoft/TypeScript/issues/10564), making this type narrowing only work when `strictNullChecks` is turned on._ ```typescript let result: Result<number, Error> = Ok(1); if (result.ok) { // Typescript knows that result.val is a number because result.ok was true let number = result.val + 1; } else { // Typescript knows that result.val is an `Error` because result.ok was false console.error(result.val.message); } if (result.err) { // Typescript knows that result.val is an `Error` because result.err was true console.error(result.val.message); } else { // Typescript knows that result.val is a number because result.err was false let number = result.val + 1; } ``` ### Stack Trace A stack trace is generated when an `Err` is created. ```typescript let error = Err('Uh Oh'); let stack = error.stack; ``` #### Unwrap ```typescript let goodResult = new Ok(1); let badResult = new Err(new Error('something went wrong')); goodResult.unwrap(); // 1 badResult.unwrap(); // throws Error("something went wrong") ``` #### Expect ```typescript let goodResult = Ok(1); let badResult = Err(new Error('something went wrong')); goodResult.expect('goodResult should be a number'); // 1 badResult.expect('badResult should be a number'); // throws Error("badResult should be a number - Error: something went wrong") ``` #### Map and MapErr ```typescript let goodResult = Ok(1); let badResult = Err(new Error('something went wrong')); goodResult.map((num) => num + 1).unwrap(); // 2 badResult.map((num) => num + 1).unwrap(); // throws Error("something went wrong") goodResult .map((num) => num + 1) .mapErr((err) => new Error('mapped')) .unwrap(); // 2 badResult .map((num) => num + 1) .mapErr((err) => new Error('mapped')) .unwrap(); // throws Error("mapped") ``` #### Else Deprecated in favor of unwrapOr #### UnwrapOr ```typescript let goodResult = Ok(1); let badResult = Err(new Error('something went wrong')); goodResult.unwrapOr(5); // 1 badResult.unwrapOr(5); // 5 ``` #### Empty ```typescript function checkIsValid(isValid: boolean): Result<void, Error> { if (isValid) { return Ok.EMPTY; } else { return new Err(new Error('Not valid')); } } ``` #### Combining Results `results` has two helper functions for operating over n `Result` objects. ##### Result.all Either returns all of the `Ok` values, or the first `Err` value ```typescript let pizzaResult: Result<Pizza, GetPizzaError> = getPizzaSomehow(); let toppingsResult: Result<Toppings, GetToppingsError> = getToppingsSomehow(); let result = Result.all(pizzaResult, toppingsResult); // Result<[Pizza, Toppings], GetPizzaError | GetToppingsError> let [pizza, toppings] = result.unwrap(); // pizza is a Pizza, toppings is a Toppings. Could throw GetPizzaError or GetToppingsError. ``` ##### Result.any Either returns the first `Ok` value, or all `Err` values ```typescript let url1: Result<string, Error1> = attempt1(); let url2: Result<string, Error2> = attempt2(); let url3: Result<string, Error3> = attempt3(); let result = Result.any(url1, url2, url3); // Result<string, Error1 | Error2 | Error3> let url = result.unwrap(); // At least one attempt gave us a successful url ``` ## Usage with rxjs #### resultMap Allows you to do the same actions as the normal [rxjs map](http://reactivex.io/documentation/operators/map.html) operator on a stream of Result objects. ```typescript import { of, Observable } from 'rxjs'; import { Ok, Err, Result } from '@utx/result'; import { resultMap } from '@utx/result/rxjs-operators'; const obs$: Observable<Result<number, Error>> = of(Ok(5), Err('uh oh')); const greaterThanZero = obs$.pipe( resultMap((number) => number > 0), // Doubles the value ); // Has type Observable<Result<boolean, 'uh oh'>> greaterThanZero.subscribe((result) => { if (result.ok) { console.log('Was greater than zero: ' + result.val); } else { console.log('Got Error Message: ' + result.val); } }); // Logs the following: // Got number: 10 // Got Error Message: uh oh ``` #### resultMapErr ```typescript import { resultMapErr } from '@utx/result/rxjs-operators'; ``` Behaves exactly the same as [resultMap](#resultmap), but maps the error value. #### resultMapTo ```typescript import { resultMapTo } from '@utx/result/rxjs-operators'; ``` Behaves the same as [resultMap](#resultmap), but takes a value instead of a function. #### resultMapErrTo ```typescript import { resultMapErrTo } from '@utx/result/rxjs-operators'; ``` Behaves the same as [resultMapErr](#resultmaperr), but takes a value instead of a function. #### elseMap Allows you to turn a stream of Result objects into a stream of values, transforming any errors into a value. Similar to calling the [else](#else) function, but works on a stream of Result objects. ```typescript import { of, Observable } from 'rxjs'; import { Ok, Err, Result } from '@utx/result'; import { elseMap } from '@utx/result/rxjs-operators'; const obs$: Observable<Result<number, Error>> = of(Ok(5), Err(new Error('uh oh'))); const doubled = obs$.pipe( elseMap((err) => { console.log('Got error: ' + err.message); return -1; }), ); // Has type Observable<number> doubled.subscribe((number) => { console.log('Got number: ' + number); }); // Logs the following: // Got number: 5 // Got error: uh oh // Got number: -1 ``` #### elseMapTo ```typescript import { elseMapTo } from '@utx/result/rxjs-operators'; ``` Behaves the same as [elseMap](#elsemap), but takes a value instead of a function. #### resultSwitchMap and resultMergeMap Allows you to do the same actions as the normal [rxjs switchMap](https://www.learnrxjs.io/operators/transformation/switchmap.html) and [rxjs switchMap](https://www.learnrxjs.io/operators/transformation/mergemap.html) operator on a stream of Result objects. Merging or switching from a stream of `Result<T, E>` objects onto a stream of `<T2>` objects turns the stream into a stream of `Result<T2, E>` objects. Merging or switching from a stream of `Result<T, E>` objects onto a stream of `Result<T2, E2>` objects turn the stream into a stream of `Result<T2, E | T2>` objects. ```typescript import { of, Observable } from 'rxjs'; import { Ok, Err, Result } from '@utx/result'; import { resultMergeMap } from '@utx/result/rxjs-operators'; const obs$: Observable<Result<number, Error>> = of(new Ok(5), new Err(new Error('uh oh'))); const obs2$: Observable<Result<string, CustomError>> = of(new Ok('hi'), new Err(new CustomError('custom error'))); const test$ = obs$.pipe( resultMergeMap((number) => { console.log('Got number: ' + number); return obs2$; }), ); // Has type Observable<Result<string, CustomError | Error>> test$.subscribe((result) => { if (result.ok) { console.log('Got string: ' + result.val); } else { console.log('Got error: ' + result.val.message); } }); // Logs the following: // Got number: 5 // Got string: hi // Got error: custom error // Got error: uh oh ``` #### filterResultOk Converts an `Observable<Result<T, E>>` to an `Observble<T>` by filtering out the Errs and mapping to the Ok values. ```typescript import { of, Observable } from 'rxjs'; import { Ok, Err, Result } from '@utx/result'; import { filterResultOk } from '@utx/result/rxjs-operators'; const obs$: Observable<Result<number, Error>> = of(new Ok(5), new Err(new Error('uh oh'))); const test$ = obs$.pipe(filterResultOk()); // Has type Observable<number> test$.subscribe((result) => { console.log('Got number: ' + result); }); // Logs the following: // Got number: 5 ``` #### filterResultErr Converts an `Observable<Result<T, E>>` to an `Observble<T>` by filtering out the Oks and mapping to the error values. ```typescript import { of, Observable } from 'rxjs'; import { Ok, Err, Result } from '@utx/result'; import { filterResultOk } from '@utx/result/rxjs-operators'; const obs$: Observable<Result<number, Error>> = of(new Ok(5), new Err(new Error('uh oh'))); const test$ = obs$.pipe(filterResultOk()); // Has type Observable<number> test$.subscribe((result) => { console.log('Got number: ' + result); }); // Logs the following: // Got number: 5 ```