A typescript implementation of Rust's Result and Option objects.
Brings compile-time error checking and optional values to typescript.
This package is a friendly fork of the excellent https://github.com/vultix/ts-results/ created due to time constraints on our (Lune's) side – we needed a package available with some fixes.
Notable changes compared to the original package:
- Added ESM compatibility
Option
gained extra methods:mapOr()
,mapOrElse()
,or()
,orElse()
Result
also gained extra methods:mapOr()
,mapOrElse()
,expectErr()
,or()
,orElse()
Ok
andErr
no longer have theval
property – it'sOk.value
andErr.error
now- There is
Some.value
which replacedSome.val
- Boolean flags were replaced with methods:
Option.some
->Option.isSome()
Option.none
->Option.isNone()
Result.ok
->Result.isOk()
Result.err
->Result.isErr()
We'll try to get the changes merged into the upstream package so that this fork can become obsolete.
$ npm install ts-results-es
or
$ yarn add ts-results-es
Convert this:
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:
import { existsSync, readFileSync } from 'fs';
import { Ok, Err, Result } from 'ts-results-es';
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.isOk()) {
// text contains the file's content
const text = result.value;
} else {
// err equals 'invalid path'
const err = result.error;
}
Convert this:
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:
import { Option, Some, None } from 'ts-results-es';
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.value;
}
import { Result, Err, Ok } from 'ts-results-es';
let okResult: Result<number, Error> = Ok(10);
let errorResult: Result<number, Error> = Err(new Error('bad number!'));
Note: Typescript currently has a bug, making this type narrowing only work when strictNullChecks
is turned on.
let result: Result<number, Error> = Ok(1);
if (result.isOk()) {
// Typescript knows that result.value is a number because result.isOk() was true
let number = result.value + 1;
} else {
// Typescript knows that result.error is an `Error` because result.isOk() was false
console.error(result.error.message);
}
if (result.isErr()) {
// Typescript knows that result.error is an `Error` because result.isErr() was true
console.error(result.error.message);
} else {
// Typescript knows that result.value is a number because result.isErr() was false
let number = result.value + 1;
}
A stack trace is generated when an Err
is created.
let error = Err('Uh Oh');
let stack = error.stack;
let goodResult = new Ok(1);
let badResult = new Err(new Error('something went wrong'));
goodResult.unwrap(); // 1
badResult.unwrap(); // throws Error("something went wrong")
let goodResult = new Ok(1);
let badResult = new Err('something went wrong');
goodResult.unwrapErr(); // throws an exception
badResult.unwrapErr(); // returns 'something went wrong'
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")
let goodResult = Ok(1);
let badResult = Err(new Error('something went wrong'));
goodResult.expectErr('goodResult should not be a number'); // throws Error("goodResult should not be a number")
badResult.expectErr('badResult should not be a number'); // new Error('something went wrong')
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")
let goodResult = Ok(1);
let badResult = Err(new Error('something went wrong'));
goodResult.mapOr(0, (value) => -value) // -1
badResult.mapOr(0, (value) => -value) // 0
// mapOrElse() is useful when you only want to call the default function
// when it's necessary (if it performs some heavy computation for example).
goodResult.mapOrElse((_error) => 0, (value) => -value) // -1
badResult.mapOrElse((_error) => 0, (value) => -value) // 0
let goodResult = Ok(1);
let badResult = Err(new Error('something went wrong'));
goodResult.or(Ok(2)) // 1
badResult.or(Ok(2)) // 2
// orElse() is useful when you only want to call a function to get the
// value when it's necessary (if it performs some heavy computation
// for example).
goodResult.orElse((_error) => Ok(2)) // 1, the function is *not* called.
badResult.orElse((_error) => Ok(2)) // 2
let goodResult = Ok(1);
let badResult = Err(new Error('something went wrong'));
goodResult.andThen((num) => new Ok(num + 1)).unwrap(); // 2
badResult.andThen((num) => new Err(new Error('2nd error'))).unwrap(); // throws Error('something went wrong')
goodResult.andThen((num) => new Err(new Error('2nd error'))).unwrap(); // throws Error('2nd error')
goodResult
.andThen((num) => new Ok(num + 1))
.mapErr((err) => new Error('mapped'))
.unwrap(); // 2
badResult
.andThen((num) => new Err(new Error('2nd error')))
.mapErr((err) => new Error('mapped'))
.unwrap(); // throws Error('mapped')
goodResult
.andThen((num) => new Err(new Error('2nd error')))
.mapErr((err) => new Error('mapped'))
.unwrap(); // throws Error('mapped')
Deprecated in favor of unwrapOr
let goodResult = Ok(1);
let badResult = Err(new Error('something went wrong'));
goodResult.unwrapOr(5); // 1
badResult.unwrapOr(5); // 5
function checkIsValid(isValid: boolean): Result<void, Error> {
if (isValid) {
return Ok.EMPTY;
} else {
return new Err(new Error('Not valid'));
}
}
ts-results
has two helper functions for operating over n Result
objects.
Either returns all of the Ok
values, or the first Err
value
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.
Either returns the first Ok
value, or all Err
values
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
Allows you to do the same actions as the normal rxjs map operator on a stream of Result objects.
import { of, Observable } from 'rxjs';
import { Ok, Err, Result } from 'ts-results-es';
import { resultMap } from 'ts-results-es/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.isOk()) {
console.log('Was greater than zero: ' + result.value);
} else {
console.log('Got Error Message: ' + result.error);
}
});
// Logs the following:
// Got number: 10
// Got Error Message: uh oh
import { resultMapErr } from 'ts-results-es/rxjs-operators';
Behaves exactly the same as resultMap, but maps the error value.
import { resultMapTo } from 'ts-results-es/rxjs-operators';
Behaves the same as resultMap, but takes a value instead of a function.
import { resultMapErrTo } from 'ts-results-es/rxjs-operators';
Behaves the same as resultMapErr, but takes a value instead of a function.
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 function, but works on a stream of Result objects.
import { of, Observable } from 'rxjs';
import { Ok, Err, Result } from 'ts-results-es';
import { elseMap } from 'ts-results-es/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
import { elseMapTo } from 'ts-results-es/rxjs-operators';
Behaves the same as elseMap, but takes a value instead of a function.
Allows you to do the same actions as the normal rxjs switchMap and rxjs switchMap 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.
import { of, Observable } from 'rxjs';
import { Ok, Err, Result } from 'ts-results-es';
import { resultMergeMap } from 'ts-results-es/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.isOk()) {
console.log('Got string: ' + result.value);
} else {
console.log('Got error: ' + result.error.message);
}
});
// Logs the following:
// Got number: 5
// Got string: hi
// Got error: custom error
// Got error: uh oh
Converts an Observable<Result<T, E>>
to an Observble<T>
by filtering out the Errs and mapping to the Ok values.
import { of, Observable } from 'rxjs';
import { Ok, Err, Result } from 'ts-results-es';
import { filterResultOk } from 'ts-results-es/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
Converts an Observable<Result<T, E>>
to an Observble<T>
by filtering out the Oks and mapping to the error values.
import { of, Observable } from 'rxjs';
import { Ok, Err, Result } from 'ts-results-es';
import { filterResultOk } from 'ts-results-es/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
The package is published manually right now.
Steps to publish:
- Bump the version in
package.json
andsrc/package.json
as needed - Update the CHANGELOG
- Commit to Git in a single commit and add a tag:
git tag -a vX.X.X
(the tag description can be anything) npm run build && npm publish
- Push both the
master
branch and the new tag to GitHub