diff --git a/README.md b/README.md index 0a36c081..0307b82b 100644 --- a/README.md +++ b/README.md @@ -18,15 +18,17 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a ## Table Of Contents -* [Installation](#installation) -* [Recommended: Use `eslint-plugin-neverthrow`](#recommended-use-eslint-plugin-neverthrow) -* [Top-Level API](#top-level-api) -* [API Documentation](#api-documentation) - + [Synchronous API (`Result`)](#synchronous-api-result) +- [Installation](#installation) +- [Recommended: Use `eslint-plugin-neverthrow`](#recommended-use-eslint-plugin-neverthrow) +- [Top-Level API](#top-level-api) +- [API Documentation](#api-documentation) + - [Synchronous API (`Result`)](#synchronous-api-result) - [`ok`](#ok) - [`err`](#err) - [`Result.isOk` (method)](#resultisok-method) - [`Result.isErr` (method)](#resultiserr-method) + - [`Result.and` (method)](#resultand-method) + - [`Result.or` (method)](#resultor-method) - [`Result.map` (method)](#resultmap-method) - [`Result.mapErr` (method)](#resultmaperr-method) - [`Result.unwrapOr` (method)](#resultunwrapor-method) @@ -43,12 +45,14 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a - [`Result.combine` (static class method)](#resultcombine-static-class-method) - [`Result.combineWithAllErrors` (static class method)](#resultcombinewithallerrors-static-class-method) - [`Result.safeUnwrap()`](#resultsafeunwrap) - + [Asynchronous API (`ResultAsync`)](#asynchronous-api-resultasync) + - [Asynchronous API (`ResultAsync`)](#asynchronous-api-resultasync) - [`okAsync`](#okasync) - [`errAsync`](#errasync) - [`ResultAsync.fromThrowable` (static class method)](#resultasyncfromthrowable-static-class-method) - [`ResultAsync.fromPromise` (static class method)](#resultasyncfrompromise-static-class-method) - [`ResultAsync.fromSafePromise` (static class method)](#resultasyncfromsafepromise-static-class-method) + - [`ResultAsync.and` (method)](#resultasyncand-method) + - [`ResultAsync.or` (method)](#resultasyncor-method) - [`ResultAsync.map` (method)](#resultasyncmap-method) - [`ResultAsync.mapErr` (method)](#resultasyncmaperr-method) - [`ResultAsync.unwrapOr` (method)](#resultasyncunwrapor-method) @@ -61,14 +65,14 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a - [`ResultAsync.combine` (static class method)](#resultasynccombine-static-class-method) - [`ResultAsync.combineWithAllErrors` (static class method)](#resultasynccombinewithallerrors-static-class-method) - [`ResultAsync.safeUnwrap()`](#resultasyncsafeunwrap) - + [Utilities](#utilities) + - [Utilities](#utilities) - [`fromThrowable`](#fromthrowable) - [`fromAsyncThrowable`](#fromasyncthrowable) - [`fromPromise`](#frompromise) - [`fromSafePromise`](#fromsafepromise) - [`safeTry`](#safetry) - + [Testing](#testing) -* [A note on the Package Name](#a-note-on-the-package-name) + - [Testing](#testing) +- [A note on the Package Name](#a-note-on-the-package-name) ## Installation @@ -94,8 +98,7 @@ With `eslint-plugin-neverthrow`, you are forced to consume the result in one of This ensures that you're explicitly handling the error of your `Result`. -This plugin is essentially a porting of Rust's [`must-use`](https://doc.rust-lang.org/std/result/#results-must-be-used) attribute. - +This plugin is essentially a porting of Rust's [`must-use`](https://doc.rust-lang.org/std/result/#results-must-be-used) attribute. ## Top-Level API @@ -161,7 +164,7 @@ myResult.isOk() // true myResult.isErr() // false ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -186,7 +189,7 @@ myResult.isOk() // false myResult.isErr() // true ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -200,7 +203,7 @@ Returns `true` if the result is an `Ok` variant isOk(): boolean { ... } ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -214,7 +217,102 @@ Returns `true` if the result is an `Err` variant isErr(): boolean { ... } ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) + +--- + +#### `Result.and` (method) + +Returns a default value wrapped in an `Ok` if the result is an `Ok` variant. +Otherwise, returns the `Err` value untouched. + +Useful for when there is a default `Ok` value you want to return +as long as the previous computation was successful. +A more elegant way to do `.map(() => defaultValue)`. + +**Signature**: + +```typescript +class Result { + and(defaultValue: A): Result { ... } +} +``` + +**Example**: + +```typescript +import { centerMapOnLocation } from './imaginary-map-utils' + +const defaultMapLocation = { lat: 0, lng: 0 } +const countryLocations = [ + { name: 'Japan', location: { lat: 45.5, lng: -73.5 } }, + { name: 'France', location: { lat: 37.5, lng: -122.5 } }, + { name: 'Congo', location: { lat: 19.5, lng: -99.5 } }, +] + +const addCountry = (newCountry: { + name: string + location: { lat: number; lng: number } +}): Result => { + if (countries.find((country) => country.name === newCountry.name)) { + return err('Country already exists') + } + countries.push(newCountry) + return ok() +} + +const newCountry1 = { name: 'New Zealand', location: { lat: 12, lng: 42 } } +addCountry(newCountry).and(newCountry1.name) // Ok('New Zealand') + +const newCountry2 = { name: 'Japan', location: { lat: 56, lng: 55 } } +addCountry(newCountry).and(newCountry2.name) // Err('Country already exists') +``` + +[⬆️ Back to top](#toc) + +--- + +#### `Result.or` (method) + +If the result is an `Ok` value, returns it untouched. +Else, returns a default value wrapped inside an `Ok`. + +Useful for when you want to return a default `Ok` value +instead of an `Err` value. +A more elegant way to do `.orElse(() => ok(defaultValue))`. + +**Signature**: + +```typescript +class Result { + or(defaultValue: A): Result { ... } +} +``` + +**Example**: + +```typescript +import { centerMapOnLocation } from './imaginary-map-utils' + +const defaultMapLocation = { lat: 0, lng: 0 } +const countries = [ + { name: 'Japan', location: { lat: 45.5, lng: -73.5 } }, + { name: 'France', location: { lat: 37.5, lng: -122.5 } }, + { name: 'Congo', location: { lat: 19.5, lng: -99.5 } }, +] + +const getCountryLocation = (countryName: string): Result => { + const country = countries.find((country) => country.name === countryName) + return country ? ok(country.location) : err('Country not found') +} + +const currentCountry = 'New Zealand' + +// Since the country can't be found, the default location is used +getCountryLoation(currentCountry).or(defaultMapLocation).andTee(centerMapOnLocation) +``` + +[⬆️ Back to top](#toc) --- @@ -245,14 +343,12 @@ import { getLines } from 'imaginary-parser' const linesResult = getLines('1\n2\n3\n4\n') // this Result now has a Array inside it -const newResult = linesResult.map( - (arr: Array) => arr.map(parseInt) -) +const newResult = linesResult.map((arr: Array) => arr.map(parseInt)) newResult.isOk() // true ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -281,16 +377,16 @@ const rawHeaders = 'nonsensical gibberish and badly formatted stuff' const parseResult = parseHeaders(rawHeaders) -parseResult.mapErr(parseError => { +parseResult.mapErr((parseError) => { res.status(400).json({ - error: parseError + error: parseError, }) }) parseResult.isErr() // true ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -316,7 +412,7 @@ const multiply = (value: number): number => value * 2 const unwrapped: number = myResult.map(multiply).unwrapOr(10) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -350,21 +446,13 @@ import { err, ok } from 'neverthrow' const sq = (n: number): Result => ok(n ** 2) -ok(2) - .andThen(sq) - .andThen(sq) // Ok(16) +ok(2).andThen(sq).andThen(sq) // Ok(16) -ok(2) - .andThen(sq) - .andThen(err) // Err(4) +ok(2).andThen(sq).andThen(err) // Err(4) -ok(2) - .andThen(err) - .andThen(sq) // Err(2) +ok(2).andThen(err).andThen(sq) // Err(2) -err(3) - .andThen(sq) - .andThen(sq) // Err(3) +err(3).andThen(sq).andThen(sq) // Err(3) ``` **Example 2: Flattening Nested Results** @@ -377,7 +465,7 @@ const nested = ok(ok(1234)) const notNested = nested.andThen((innerResult) => innerResult) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -397,7 +485,7 @@ class Result { } ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -428,17 +516,17 @@ const dbQueryResult: Result = err(DatabaseError.NotFound) const updatedQueryResult = dbQueryResult.orElse((dbError) => dbError === DatabaseError.NotFound ? ok('User does not exist') // error recovery branch: ok() must be called with a value of type string - // - // - // err() can be called with a value of any new type that you want - // it could also be called with the same error value - // - // err(dbError) - : err(500) + : // + // + // err() can be called with a value of any new type that you want + // it could also be called with the same error value + // + // err(dbError) + err(500), ) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -461,6 +549,7 @@ class Result { `match` is like chaining `map` and `mapErr`, with the distinction that with `match` both functions must have the same return type. The differences between `match` and chaining `map` and `mapErr` are that: + - with `match` both functions must have the same return type `A` - `match` unwraps the `Result` into an `A` (the match functions' return type) - This makes no difference if you are performing side effects only @@ -488,16 +577,17 @@ const attempt = computationThatMightFail() const answer = computationThatMightFail().match( (str) => str.toUpperCase(), - (err) => `Error: ${err}` + (err) => `Error: ${err}`, ) // `answer` is of type `string` ``` If you don't use the error parameter in your match callback then `match` is equivalent to chaining `map` with `unwrapOr`: + ```ts const answer = computationThatMightFail().match( (str) => str.toUpperCase(), - () => 'ComputationError' + () => 'ComputationError', ) // `answer` is of type `string` @@ -506,8 +596,7 @@ const answer = computationThatMightFail() .unwrapOr('ComputationError') ``` - -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -538,20 +627,20 @@ import { parseHeaders } from 'imaginary-http-parser' // parseHeaders(raw: string): Result const asyncRes = parseHeaders(rawHeader) - .map(headerKvMap => headerKvMap.Authorization) + .map((headerKvMap) => headerKvMap.Authorization) .asyncMap(findUserInDatabase) ``` Note that in the above example if `parseHeaders` returns an `Err` then `.map` and `.asyncMap` will not be invoked, and `asyncRes` variable will resolve to an `Err` when turned into a `Result` using `await` or `.then()`. - -[⬆️ Back to top](#toc) + +[⬆️ Back to top](#toc) --- #### `Result.andTee` (method) Takes a `Result` and lets the original `Result` pass through regardless the result of the passed-in function. -This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging. +This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging. **Signature:** @@ -572,26 +661,23 @@ import { insertUser } from 'imaginary-database' // ^ assume parseUserInput, logUser and insertUser have the following signatures: // parseUserInput(input: RequestData): Result -// logUser(user: User): Result +// logUser(user: User): Result // insertUser(user: User): ResultAsync // Note logUser returns void upon success but insertUser takes User type. -const resAsync = parseUserInput(userInput) - .andTee(logUser) - .asyncAndThen(insertUser) +const resAsync = parseUserInput(userInput).andTee(logUser).asyncAndThen(insertUser) // Note no LogError shows up in the Result type resAsync.then((res: Result) => { - if(res.isErr()){ - console.log("Oops, at least one step failed", res.error) - } - else{ - console.log("User input has been parsed and inserted successfully.") + if (res.isErr()) { + console.log('Oops, at least one step failed', res.error) + } else { + console.log('User input has been parsed and inserted successfully.') } }) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -619,26 +705,23 @@ import { insertUser } from 'imaginary-database' // ^ assume parseUserInput, logParseError and insertUser have the following signatures: // parseUserInput(input: RequestData): Result -// logParseError(parseError: ParseError): Result +// logParseError(parseError: ParseError): Result // insertUser(user: User): ResultAsync // Note logParseError returns void upon success but insertUser takes User type. -const resAsync = parseUserInput(userInput) - .orTee(logParseError) - .asyncAndThen(insertUser) +const resAsync = parseUserInput(userInput).orTee(logParseError).asyncAndThen(insertUser) // Note no LogError shows up in the Result type resAsync.then((res: Result) => { - if(res.isErr()){ - console.log("Oops, at least one step failed", res.error) - } - else{ - console.log("User input has been parsed and inserted successfully.") + if (res.isErr()) { + console.log('Oops, at least one step failed', res.error) + } else { + console.log('User input has been parsed and inserted successfully.') } }) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -669,29 +752,26 @@ import { insertUser } from 'imaginary-database' // parseUserInput(input: RequestData): Result // validateUser(user: User): Result // insertUser(user: User): ResultAsync -// Note validateUser returns void upon success but insertUser takes User type. +// Note validateUser returns void upon success but insertUser takes User type. -const resAsync = parseUserInput(userInput) - .andThrough(validateUser) - .asyncAndThen(insertUser) +const resAsync = parseUserInput(userInput).andThrough(validateUser).asyncAndThen(insertUser) resAsync.then((res: Result) => { - if(res.isErr()){ - console.log("Oops, at least one step failed", res.error) - } - else{ - console.log("User input has been parsed, validated, inserted successfully.") + if (res.isErr()) { + console.log('Oops, at least one step failed', res.error) + } else { + console.log('User input has been parsed, validated, inserted successfully.') } }) ``` - -[⬆️ Back to top](#toc) + +[⬆️ Back to top](#toc) --- #### `Result.asyncAndThrough` (method) -Similar to `andThrough` except you must return a ResultAsync. +Similar to `andThrough` except you must return a ResultAsync. You can then chain the result of `asyncAndThrough` using the `ResultAsync` apis (like `map`, `mapErr`, `andThen`, etc.) @@ -706,25 +786,23 @@ import { sendNotification } from 'imaginary-service' // parseUserInput(input: RequestData): Result // insertUser(user: User): ResultAsync // sendNotification(user: User): ResultAsync -// Note insertUser returns void upon success but sendNotification takes User type. +// Note insertUser returns void upon success but sendNotification takes User type. -const resAsync = parseUserInput(userInput) - .asyncAndThrough(insertUser) - .andThen(sendNotification) +const resAsync = parseUserInput(userInput).asyncAndThrough(insertUser).andThen(sendNotification) resAsync.then((res: Result) => { - if(res.isErr()){ - console.log("Oops, at least one step failed", res.error) - } - else{ - console.log("User has been parsed, inserted and notified successfully.") + if (res.isErr()) { + console.log('Oops, at least one step failed', res.error) + } else { + console.log('User has been parsed, inserted and notified successfully.') } }) ``` - -[⬆️ Back to top](#toc) + +[⬆️ Back to top](#toc) --- + #### `Result.fromThrowable` (static class method) > Although Result is not an actual JS class, the way that `fromThrowable` has been implemented requires that you call `fromThrowable` as though it were a static method on `Result`. See examples below. @@ -746,15 +824,15 @@ map what is thrown to a known type. import { Result } from 'neverthrow' type ParseError = { message: string } -const toParseError = (): ParseError => ({ message: "Parse Error" }) +const toParseError = (): ParseError => ({ message: 'Parse Error' }) const safeJsonParse = Result.fromThrowable(JSON.parse, toParseError) // the function can now be used safely, if the function throws, the result will be an Err -const res = safeJsonParse("{"); +const res = safeJsonParse('{') ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -787,27 +865,25 @@ function combine => Result<[ T1, T2, T3, T4 ], E ``` Example: + ```typescript -const resultList: Result[] = - [ok(1), ok(2)] +const resultList: Result[] = [ok(1), ok(2)] -const combinedList: Result = - Result.combine(resultList) +const combinedList: Result = Result.combine(resultList) ``` Example with tuples: + ```typescript /** @example tuple(1, 2, 3) === [1, 2, 3] // with type [number, number, number] */ const tuple = (...args: T): T => args -const resultTuple: [Result, Result] = - tuple(ok('a'), ok('b')) +const resultTuple: [Result, Result] = tuple(ok('a'), ok('b')) -const combinedTuple: Result<[string, string], unknown> = - Result.combine(resultTuple) +const combinedTuple: Result<[string, string], unknown> = Result.combine(resultTuple) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -835,19 +911,14 @@ function combineWithAllErrors => Result<[ T1, T2 Example usage: ```typescript -const resultList: Result[] = [ - ok(123), - err('boooom!'), - ok(456), - err('ahhhhh!'), -] +const resultList: Result[] = [ok(123), err('boooom!'), ok(456), err('ahhhhh!')] const result = Result.combineWithAllErrors(resultList) // result is Err(['boooom!', 'ahhhhh!']) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) #### `Result.safeUnwrap()` @@ -855,8 +926,7 @@ const result = Result.combineWithAllErrors(resultList) Allows for unwrapping a `Result` or returning an `Err` implicitly, thereby reducing boilerplate. - -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -885,7 +955,7 @@ myResult.isOk() // true myResult.isErr() // false ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -912,7 +982,7 @@ myResult.isOk() // false myResult.isErr() // true ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -953,7 +1023,7 @@ const insertUser = (user: User): Promise => { const res = ResultAsync.fromPromise(insertIntoDb(myUser), () => new Error('Database error')) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -963,7 +1033,6 @@ Transforms a `PromiseLike` (that may throw) into a `ResultAsync`. The second argument handles the rejection case of the promise and maps the error from `unknown` into some type `E`. - **Signature:** ```typescript @@ -989,7 +1058,7 @@ const res = ResultAsync.fromPromise(insertIntoDb(myUser), () => new Error('Datab // `res` has a type of ResultAsync ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -1021,7 +1090,7 @@ export const slowDown = (ms: number) => (value: T) => setTimeout(() => { resolve(value) }, ms) - }) + }), ) export const signupHandler = route((req, sessionManager) => @@ -1030,11 +1099,81 @@ export const signupHandler = route((req, sessionManager) => .andThen(slowDown(3000)) // slowdown by 3 seconds .andThen(sessionManager.createSession) .map(({ sessionToken, admin }) => AppData.init(admin, sessionToken)) - }) + }), ) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) + +--- + +#### `ResultAsync.and` (method) + +Returns a default value wrapped in an `Ok` inside a `Promise` if the `ResultAsync` resolves to an `Ok` variant. +Otherwise, resolves to the `Err` value untouched. + +Useful when you want to proceed with a known default value as long as the asynchronous computation succeeds. +A more elegant way to do `.map(() => defaultValue)`. + +**Signature:** + +```typescript +class ResultAsync { + and(defaultValue: A): ResultAsync { ... } +} +``` + +**Example**: + +```typescript +const fetchUser = (id: number): ResultAsync => + id === 1 ? okAsync({ name: 'Alice', id: 1 }) : errAsync(new Error('User not found')) + +const defaultUsername = 'Guest' + +// If user is found, `Ok('Guest')` is passed to `someOtherFunction` +fetchUser(1).and(defaultUsername).andThen(someOtherFunction) + +// If user not found, propagate the error +fetchUser(42).and(defaultUsername).andThen(someOtherFunction) +``` + +[⬆️ Back to top](#toc) + +--- + +#### `ResultAsync.or` (method) + +If the `ResultAsync` resolves to an `Ok` value, it remains untouched. +If it resolves to an Err, returns the given default value wrapped in an Ok inside a Promise. + +Useful when you want to recover from a failed async operation with a known default `Ok` value. +A more elegant way to do `.orElse(() => okAsync(defaultValue))`. + +**Signature:** + +```typescript +class ResultAsync { + or(defaultValue: A): ResultAsync { ... } +} +``` + +**Example**: + +```typescript +const fetchCountryLocation = (countryName: string): ResultAsync => { + const country = countries.find((c) => c.name === countryName)?.location + return country ? okAsync(country.location) : errAsync(new Error('Country not found')) +} + +const defaultMapLocation = { lat: 0, lng: 0 } + +// If country is found, center the map on the location +// If not, center the map on the `defaultMapLocation` +fetchCountryLocation('Atlantis').or(defaultMapLocation).andThrough(centerMapOnLocation) +``` + +[⬆️ Back to top](#toc) --- @@ -1063,24 +1202,23 @@ import { findUsersIn } from 'imaginary-database' // ^ assume findUsersIn has the following signature: // findUsersIn(country: string): ResultAsync, Error> -const usersInCanada = findUsersIn("Canada") +const usersInCanada = findUsersIn('Canada') // Let's assume we only need their names -const namesInCanada = usersInCanada.map((users: Array) => users.map(user => user.name)) +const namesInCanada = usersInCanada.map((users: Array) => users.map((user) => user.name)) // namesInCanada is of type ResultAsync, Error> // We can extract the Result using .then() or await namesInCanada.then((namesResult: Result, Error>) => { - if(namesResult.isErr()){ + if (namesResult.isErr()) { console.log("Couldn't get the users from the database", namesResult.error) - } - else{ - console.log("Users in Canada are named: " + namesResult.value.join(',')) + } else { + console.log('Users in Canada are named: ' + namesResult.value.join(',')) } }) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -1110,32 +1248,31 @@ import { findUsersIn } from 'imaginary-database' // findUsersIn(country: string): ResultAsync, Error> // Let's say we need to low-level errors from findUsersIn to be more readable -const usersInCanada = findUsersIn("Canada").mapErr((error: Error) => { +const usersInCanada = findUsersIn('Canada').mapErr((error: Error) => { // The only error we want to pass to the user is "Unknown country" - if(error.message === "Unknown country"){ + if (error.message === 'Unknown country') { return error.message } // All other errors will be labelled as a system error - return "System error, please contact an administrator." + return 'System error, please contact an administrator.' }) // usersInCanada is of type ResultAsync, string> usersInCanada.then((usersResult: Result, string>) => { - if(usersResult.isErr()){ + if (usersResult.isErr()) { res.status(400).json({ - error: usersResult.error + error: usersResult.error, }) - } - else{ + } else { res.status(200).json({ - users: usersResult.value + users: usersResult.value, }) } }) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -1159,7 +1296,7 @@ const unwrapped: number = await errAsync(0).unwrapOr(10) // unwrapped = 10 ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -1190,7 +1327,6 @@ class ResultAsync { **Example** ```typescript - import { validateUser } from 'imaginary-validator' import { insertUser } from 'imaginary-database' import { sendNotification } from 'imaginary-service' @@ -1200,23 +1336,20 @@ import { sendNotification } from 'imaginary-service' // insertUser(user): ResultAsync // sendNotification(user): ResultAsync -const resAsync = validateUser(user) - .andThen(insertUser) - .andThen(sendNotification) +const resAsync = validateUser(user).andThen(insertUser).andThen(sendNotification) // resAsync is a ResultAsync resAsync.then((res: Result) => { - if(res.isErr()){ - console.log("Oops, at least one step failed", res.error) - } - else{ - console.log("User has been validated, inserted and notified successfully.") + if (res.isErr()) { + console.log('Oops, at least one step failed', res.error) + } else { + console.log('User has been validated, inserted and notified successfully.') } }) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -1234,7 +1367,7 @@ class ResultAsync { } ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -1258,7 +1391,6 @@ class ResultAsync { **Example:** ```typescript - import { validateUser } from 'imaginary-validator' import { insertUser } from 'imaginary-database' @@ -1268,23 +1400,24 @@ import { insertUser } from 'imaginary-database' // Handle both cases at the end of the chain using match const resultMessage = await validateUser(user) - .andThen(insertUser) - .match( - (user: User) => `User ${user.name} has been successfully created`, - (error: Error) => `User could not be created because ${error.message}` - ) + .andThen(insertUser) + .match( + (user: User) => `User ${user.name} has been successfully created`, + (error: Error) => `User could not be created because ${error.message}`, + ) // resultMessage is a string ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- + #### `ResultAsync.andTee` (method) -Takes a `ResultAsync` and lets the original `ResultAsync` pass through regardless +Takes a `ResultAsync` and lets the original `ResultAsync` pass through regardless the result of the passed-in function. -This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging. +This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging. **Signature:** @@ -1307,31 +1440,29 @@ import { sendNotification } from 'imaginary-service' // insertUser(user: User): ResultAsync // logUser(user: User): Result // sendNotification(user: User): ResultAsync -// Note logUser returns void on success but sendNotification takes User type. +// Note logUser returns void on success but sendNotification takes User type. -const resAsync = insertUser(user) - .andTee(logUser) - .andThen(sendNotification) +const resAsync = insertUser(user).andTee(logUser).andThen(sendNotification) -// Note there is no LogError in the types below +// Note there is no LogError in the types below resAsync.then((res: Result) => { - if(res.isErr()){ - console.log("Oops, at least one step failed", res.error) - } - else{ - console.log("User has been inserted and notified successfully.") + if (res.isErr()) { + console.log('Oops, at least one step failed', res.error) + } else { + console.log('User has been inserted and notified successfully.') } }) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- + #### `ResultAsync.orTee` (method) -Like `andTee` for the error track. Takes a `ResultAsync` and lets the original `Err` value pass through regardless +Like `andTee` for the error track. Takes a `ResultAsync` and lets the original `Err` value pass through regardless the result of the passed-in function. -This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging. +This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging. **Signature:** @@ -1354,28 +1485,25 @@ import { sendNotification } from 'imaginary-service' // insertUser(user: User): ResultAsync // logInsertError(insertError: InsertError): Result // sendNotification(user: User): ResultAsync -// Note logInsertError returns void on success but sendNotification takes User type. +// Note logInsertError returns void on success but sendNotification takes User type. -const resAsync = insertUser(user) - .orTee(logUser) - .andThen(sendNotification) +const resAsync = insertUser(user).orTee(logUser).andThen(sendNotification) -// Note there is no LogError in the types below +// Note there is no LogError in the types below resAsync.then((res: Result) => { - if(res.isErr()){ - console.log("Oops, at least one step failed", res.error) - } - else{ - console.log("User has been inserted and notified successfully.") + if (res.isErr()) { + console.log('Oops, at least one step failed', res.error) + } else { + console.log('User has been inserted and notified successfully.') } }) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- -#### `ResultAsync.andThrough` (method) +#### `ResultAsync.andThrough` (method) Similar to `andTee` except for: @@ -1394,7 +1522,6 @@ class ResultAsync { **Example:** ```typescript - import { buildUser } from 'imaginary-builder' import { insertUser } from 'imaginary-database' import { sendNotification } from 'imaginary-service' @@ -1403,25 +1530,23 @@ import { sendNotification } from 'imaginary-service' // buildUser(userRaw: UserRaw): ResultAsync // insertUser(user: User): ResultAsync // sendNotification(user: User): ResultAsync -// Note insertUser returns void upon success but sendNotification takes User type. +// Note insertUser returns void upon success but sendNotification takes User type. -const resAsync = buildUser(userRaw) - .andThrough(insertUser) - .andThen(sendNotification) +const resAsync = buildUser(userRaw).andThrough(insertUser).andThen(sendNotification) resAsync.then((res: Result) => { - if(res.isErr()){ - console.log("Oops, at least one step failed", res.error) - } - else{ - console.log("User data has been built, inserted and notified successfully.") + if (res.isErr()) { + console.log('Oops, at least one step failed', res.error) + } else { + console.log('User data has been built, inserted and notified successfully.') } }) ``` -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- + #### `ResultAsync.combine` (static class method) Combine lists of `ResultAsync`s. @@ -1449,26 +1574,28 @@ function combine => ResultAsync<[ T1, T2, T3, T4 ``` Example: + ```typescript -const resultList: ResultAsync[] = - [okAsync(1), okAsync(2)] +const resultList: ResultAsync[] = [okAsync(1), okAsync(2)] -const combinedList: ResultAsync = - ResultAsync.combine(resultList) +const combinedList: ResultAsync = ResultAsync.combine(resultList) ``` Example with tuples: + ```typescript /** @example tuple(1, 2, 3) === [1, 2, 3] // with type [number, number, number] */ const tuple = (...args: T): T => args -const resultTuple: [ResultAsync, ResultAsync] = - tuple(okAsync('a'), okAsync('b')) +const resultTuple: [ResultAsync, ResultAsync] = tuple( + okAsync('a'), + okAsync('b'), +) -const combinedTuple: ResultAsync<[string, string], unknown> = - ResultAsync.combine(resultTuple) +const combinedTuple: ResultAsync<[string, string], unknown> = ResultAsync.combine(resultTuple) ``` -[⬆️ Back to top](#toc) + +[⬆️ Back to top](#toc) --- @@ -1512,7 +1639,7 @@ const result = ResultAsync.combineWithAllErrors(resultList) Allows for unwrapping a `Result` or returning an `Err` implicitly, thereby reducing boilerplate. -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -1523,110 +1650,109 @@ Allows for unwrapping a `Result` or returning an `Err` implicitly, thereby reduc Top level export of `Result.fromThrowable`. Please find documentation at [Result.fromThrowable](#resultfromthrowable-static-class-method) -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) #### `fromAsyncThrowable` Top level export of `ResultAsync.fromThrowable`. Please find documentation at [ResultAsync.fromThrowable](#resultasyncfromthrowable-static-class-method) -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) #### `fromPromise` Top level export of `ResultAsync.fromPromise`. Please find documentation at [ResultAsync.fromPromise](#resultasyncfrompromise-static-class-method) -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) #### `fromSafePromise` Top level export of `ResultAsync.fromSafePromise`. Please find documentation at [ResultAsync.fromSafePromise](#resultasyncfromsafepromise-static-class-method) -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) #### `safeTry` Used to implicitly return errors and reduce boilerplate. Let's say we are writing a function that returns a `Result`, and in that function we call some functions which also return `Result`s and we check those results to see whether we should keep going or abort. Usually, we will write like the following. + ```typescript -declare function mayFail1(): Result; -declare function mayFail2(): Result; +declare function mayFail1(): Result +declare function mayFail2(): Result function myFunc(): Result { - // We have to define a constant to hold the result to check and unwrap its value. - const result1 = mayFail1(); - if (result1.isErr()) { - return err(`aborted by an error from 1st function, ${result1.error}`); - } - const value1 = result1.value - - // Again, we need to define a constant and then check and unwrap. - const result2 = mayFail2(); - if (result2.isErr()) { - return err(`aborted by an error from 2nd function, ${result2.error}`); - } - const value2 = result2.value - - // And finally we return what we want to calculate - return ok(value1 + value2); + // We have to define a constant to hold the result to check and unwrap its value. + const result1 = mayFail1() + if (result1.isErr()) { + return err(`aborted by an error from 1st function, ${result1.error}`) + } + const value1 = result1.value + + // Again, we need to define a constant and then check and unwrap. + const result2 = mayFail2() + if (result2.isErr()) { + return err(`aborted by an error from 2nd function, ${result2.error}`) + } + const value2 = result2.value + + // And finally we return what we want to calculate + return ok(value1 + value2) } ``` + Basically, we need to define a constant for each result to check whether it's a `Ok` and read its `.value` or `.error`. With safeTry, we can state 'Return here if its an `Err`, otherwise unwrap it here and keep going.' in just one expression. + ```typescript -declare function mayFail1(): Result; -declare function mayFail2(): Result; +declare function mayFail1(): Result +declare function mayFail2(): Result function myFunc(): Result { - return safeTry(function*() { - return ok( - // If the result of mayFail1().mapErr() is an `Err`, the evaluation is - // aborted here and the enclosing `safeTry` block is evaluated to that `Err`. - // Otherwise, this `(yield* ...)` is evaluated to its `.value`. - (yield* mayFail1() - .mapErr(e => `aborted by an error from 1st function, ${e}`)) - + - // The same as above. - (yield* mayFail2() - .mapErr(e => `aborted by an error from 2nd function, ${e}`)) - ) - }) + return safeTry(function* () { + return ok( + // If the result of mayFail1().mapErr() is an `Err`, the evaluation is + // aborted here and the enclosing `safeTry` block is evaluated to that `Err`. + // Otherwise, this `(yield* ...)` is evaluated to its `.value`. + (yield* mayFail1().mapErr((e) => `aborted by an error from 1st function, ${e}`)) + + // The same as above. + (yield* mayFail2().mapErr((e) => `aborted by an error from 2nd function, ${e}`)), + ) + }) } ``` To use `safeTry`, the points are as follows. -* Wrap the entire block in a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) -* In that block, you can use `yield* ` to state 'Return `` if it's an `Err`, otherwise evaluate to its `.value`' -* Pass the generator function to `safeTry` + +- Wrap the entire block in a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) +- In that block, you can use `yield* ` to state 'Return `` if it's an `Err`, otherwise evaluate to its `.value`' +- Pass the generator function to `safeTry` You can also use [async generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function*) to pass an async block to `safeTry`. + ```typescript // You can use either Promise or ResultAsync. -declare function mayFail1(): Promise>; -declare function mayFail2(): ResultAsync; +declare function mayFail1(): Promise> +declare function mayFail2(): ResultAsync function myFunc(): Promise> { - return safeTry(async function*() { - return ok( - // You have to await if the expression is Promise - (yield* (await mayFail1()) - .mapErr(e => `aborted by an error from 1st function, ${e}`)) - + - // You can call `safeUnwrap` directly if its ResultAsync - (yield* mayFail2() - .mapErr(e => `aborted by an error from 2nd function, ${e}`)) - ) - }) + return safeTry(async function* () { + return ok( + // You have to await if the expression is Promise + (yield* (await mayFail1()).mapErr((e) => `aborted by an error from 1st function, ${e}`)) + + // You can call `safeUnwrap` directly if its ResultAsync + (yield* mayFail2().mapErr((e) => `aborted by an error from 2nd function, ${e}`)), + ) + }) } ``` For more information, see https://github.com/supermacro/neverthrow/pull/448 and https://github.com/supermacro/neverthrow/issues/444 -[⬆️ Back to top](#toc) +[⬆️ Back to top](#toc) --- @@ -1651,7 +1777,7 @@ import { ok } from 'neverthrow' // ... -expect(callSomeFunctionThatReturnsAResult("with", "some", "args")).toEqual(ok(someExpectation)); +expect(callSomeFunctionThatReturnsAResult('with', 'some', 'args')).toEqual(ok(someExpectation)) ``` By default, the thrown value does not contain a stack trace. This is because stack trace generation [makes error messages in Jest harder to understand](https://github.com/supermacro/neverthrow/pull/215). If you want stack traces to be generated, call `_unsafeUnwrap` and / or `_unsafeUnwrapErr` with a config object: diff --git a/src/result-async.ts b/src/result-async.ts index 20120d2b..59096ce0 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -86,6 +86,28 @@ export class ResultAsync implements PromiseLike> { ) as CombineResultsWithAllErrorsArrayAsync } + and(v: A): ResultAsync { + return new ResultAsync( + this._promise.then(async (res: Result) => { + if (res.isErr()) { + return new Err(res.error) + } + return new Ok(v) + }), + ) + } + + or(v: A): ResultAsync { + return new ResultAsync( + this._promise.then(async (res: Result) => { + if (res.isOk()) { + return new Ok(res.value) + } + return new Ok(v) + }), + ) + } + map(f: (t: T) => A | Promise): ResultAsync { return new ResultAsync( this._promise.then(async (res: Result) => { diff --git a/src/result.ts b/src/result.ts index ad447caa..a813f303 100644 --- a/src/result.ts +++ b/src/result.ts @@ -146,6 +146,32 @@ interface IResult { */ isErr(): this is Err + /** + * If the result is an `Ok` value, returns a default value inside an `Ok`. + * Else, leaves the `Err` value untouched. + * + * Useful for when there is a default `Ok` value you want to return + * as long as the previous computation was successful. + * A more elegant way to do `.map(() => v)`. + * + * @param v The default value to return wrapped in an `Ok` + * @returns Default value wrapped in a `Result` or the original `Err` untouched + */ + and(v: A): Result + + /** + * If the result is an `Ok` value, returns it untouched. + * Else, returns a default value wrapped inside an `Ok`. + * + * Useful for when you want to return a default `Ok` value + * instead of an `Err` value. + * A more elegant way to do `.orElse(() => ok(v))`. + * + * @param v The default value to return wrapped in an `Ok` + * @returns the original `Ok` value or the default value wrapped in a `Result` + */ + or(v: A): Result + /** * Maps a `Result` to `Result` * by applying a function to a contained `Ok` value, leaving an `Err` value @@ -320,6 +346,14 @@ export class Ok implements IResult { return !this.isOk() } + and(v: A): Result { + return ok(v) + } + + or(_v: A): Result { + return ok(this.value) + } + map(f: (t: T) => A): Result { return ok(f(this.value)) } @@ -427,6 +461,14 @@ export class Err implements IResult { return !this.isOk() } + and(_v: A): Result { + return err(this.error) + } + + or(v: A): Result { + return ok(v) + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars map(_f: (t: T) => A): Result { return err(this.error) diff --git a/tests/index.test.ts b/tests/index.test.ts index 9f089a9d..c1d124a9 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -47,6 +47,26 @@ describe('Result.Ok', () => { expect(ok(42)).not.toEqual(ok(43)) }) + it('If Ok, returns the other value as Ok value, else leave the error untouched', () => { + expect(ok(12).and(42)).toEqual(ok(42)) + expect(err('Wrong').and(42)).toEqual(err('Wrong')) + }) + + it('If Ok, leaves the Ok value untouched, else returns the other value as Ok value', () => { + expect(ok(12).or(42)).toEqual(ok(12)) + expect(err('Wrong').or(42)).toEqual(ok(42)) + }) + + it('If Ok, returns the other value as Ok value, else leave the error untouched', () => { + expect(okAsync(12).and(42)).toEqual(okAsync(42)) + expect(errAsync('Wrong').and(42)).toEqual(errAsync('Wrong')) + }) + + it('If Ok, leaves the Ok value untouched, else returns the other value as Ok value', () => { + expect(okAsync(12).or(42)).toEqual(okAsync(12)) + expect(errAsync('Wrong').or(42)).toEqual(okAsync(42)) + }) + it('Maps over an Ok value', () => { const okVal = ok(12) const mapFn = vitest.fn((number) => number.toString())