ts-error-as-value
Advanced tools
Comparing version 0.1.8 to 0.1.9
@@ -10,3 +10,4 @@ { | ||
"project": [ | ||
"./tsconfig.json" | ||
"./tsconfig.json", | ||
"./tsconfig.jest.json" | ||
] | ||
@@ -13,0 +14,0 @@ } |
{ | ||
"name": "ts-error-as-value", | ||
"version": "0.1.8", | ||
"version": "0.1.9", | ||
"description": "Errors as values in typescript", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
110
README.md
### Typescript error as value | ||
### Motivation | ||
At [RollCredits](https://www.rollcredits.io/) we have all come to the conclusion that try / catch causes more headaches than it's worth, and wanted a good error as values system to use in its place. | ||
Any error-as-values library in typescript is liable for being used *a lot* within any project which adds it, so making the API as convenient as humanly possible was my primary concern. | ||
Any error-as-values library in typescript is liable for being used *a lot* within any project which adds it, so making the API as convenient as humanly possible was our primary concern. | ||
The alternative typescript libraries for achieving errors-as-values all seem to have verbose and cumbersome APIs, often wrapping all returns with class instances, and asking you to call method on them such as "isOk" or "isErr" to function. | ||
Instead of this, we leverage typescript's discriminated unions to handle most of the heavy lifting for us. This greatly (in our opinion) reduces how cumbersome the API is to use. | ||
Instead of this, we leverage typescript's discriminated unions to handle most of the heavy lifting for us. | ||
To further decrease friction for using this in your project, We have also included the ability to import the functions and types of this package into your project's global scope. | ||
To further decrease friction for using this in your project, you can also import the functions and types of this package into your project's global scope in a convenient way. | ||
@@ -27,10 +25,10 @@ --- | ||
``` | ||
This will make the functions ok, fail and withResult, as well as the types Ok, Fail and Result globally available | ||
This will make the functions ResultIs.success, ResultIs.failure and withResult, as well as the types Success, Failure and Result globally available | ||
--- | ||
## ok and fail - Basic Usage | ||
Creating `Ok` and `Fail` result objects | ||
## ResultIs.success and ResultIs.failure - Basic Usage | ||
Creating `Success` and `Failure` result objects | ||
```ts | ||
const { data, error } = ok("Hello"); | ||
const { data, error } = ResultIs.success("Hello"); | ||
if (error) { | ||
@@ -43,8 +41,10 @@ // do something with error | ||
or | ||
```ts | ||
const { data, error } = fail(new Error("Error")); | ||
const {data, error} = ResultIs.failure(new Error("Error")); | ||
if (error) { | ||
// do something with error | ||
// do something with error | ||
} else { | ||
// do something with data | ||
// do something with data | ||
} | ||
@@ -54,10 +54,11 @@ ``` | ||
Wrapping the returns from functions with `fail` for errors, and `ok` for non-error so that the function calling it receives a `Result` type. | ||
Wrapping the returns from functions with `ResultIs.failure` for errors, and `ResultIs.success` for non-error so that the function calling it receives a `Result` type. | ||
```ts | ||
// Specifying the return type here is optional, as it will be inferred without it | ||
const fnWithResult = (): Result<string, Error> => { | ||
if ("" !== "") { | ||
return ok("hello"); | ||
return ResultIs.success("hello"); | ||
} | ||
return fail(new Error("Method failed")); | ||
return ResultIs.failure(new Error("Method failed")); | ||
}; | ||
@@ -79,5 +80,5 @@ | ||
if ("" !== "") { | ||
return ok("hello"); | ||
return ResultIs.success("hello"); | ||
} | ||
return fail(new Error("Method failed")); | ||
return ResultIs.failure(new Error("Method failed")); | ||
}; | ||
@@ -88,5 +89,5 @@ | ||
if (error) { | ||
return fail(error); | ||
return ResultIs.failure(error); | ||
} | ||
return ok(data); | ||
return ResultIs.success(data); | ||
}; | ||
@@ -106,5 +107,5 @@ | ||
if ("" !== "") { | ||
return ok("hello"); | ||
return ResultIs.success("hello"); | ||
} | ||
return fail(new Error("Method failed")); | ||
return ResultIs.failure(new Error("Method failed")); | ||
}; | ||
@@ -121,5 +122,5 @@ | ||
if (error) { | ||
return fail(error); | ||
return ResultIs.failure(error); | ||
} | ||
return ok(data); | ||
return ResultIs.success(data); | ||
}; | ||
@@ -133,4 +134,4 @@ ``` | ||
withResult is a function which wraps another function and returns an `Err` result if the wrapped function throws an error, | ||
or an `Ok` result if the wrapped function does not. | ||
withResult is a function which wraps another function and returns a `Failure` result if the wrapped function throws an error, | ||
or a `Success` result if the wrapped function does not. | ||
```ts | ||
@@ -150,49 +151,26 @@ import somePkg from "package-that-throws-errors"; | ||
## Error stack | ||
One advantage of using errors as values is that we know exactly where an error was created and by whom. Taking advantage of this is still a WiP, however currently we keep track of information about all errors which we encountered in the current chain of Fail results. | ||
```ts | ||
const fn1 = () => fail(new Error("Hello")); | ||
const fn2 = () => fn1().mapErr(() => new Error("World")); | ||
const { errorStack } = fn2(); | ||
``` | ||
would produce | ||
```json | ||
[ | ||
{ "message": "Hello", "stack": "..." }, | ||
{ "message": "World", "stack": "..." } | ||
] | ||
``` | ||
--- | ||
## API | ||
```typescript | ||
type None = null; | ||
type Fail<T, E extends Error = Error> = { | ||
data: never, | ||
export type Failure<E extends Error = Error> = { | ||
data: null, | ||
error: E, | ||
unwrap(): void, // Returns the value, but throws an error if the result is an Error | ||
unwrapOr<D>(defaultValue: D): D, // Returns the value or gives you a default value if it's an error | ||
mapErr<E2 extends Error>(fn: (err: E) => E2): Err<T, E2>, // If the result is an error, map the error to another error | ||
andThen<N>(fn: (data: never) => N): Err<T, E> // If the result is not an error, map the data in it | ||
successOrThrow(): void, // Returns the value, but throws an error if the result is an Error | ||
successOrDefault<D>(defaultValue: D): D, // Returns the value or gives you a default value if it's an error | ||
transformOnFailure<E2 extends Error>(fn: (fail: E) => E2): Failure<E2>, // If the result is an error, map the error to another error | ||
transformOnSuccess<N>(fn: (data: never) => N): Failure<E> // If the result is not an error, map the data in it | ||
}; | ||
type Ok<T> = { | ||
export type Success<T> = { | ||
data: T, | ||
error: never, | ||
unwrap(): T, // Returns the value, but throws an error if the result is an Error | ||
unwrapOr<D>(defaultValue: D): T, // Returns the value or gives you a default value if it's an error | ||
mapErr<E2 extends Error>(fn: (err: never) => E2): Ok<T>, // If the result is an error, map the error to another error | ||
andThen<N>(fn: (data: T) => N): Ok<N> // If the result is not an error, map the data in it | ||
error: null, | ||
successOrThrow(): T, // Returns the value, but throws an error if the result is an Error | ||
successOrDefault<D>(defaultValue: D): T, // Returns the value or gives you a default value if it's an error | ||
transformOnFailure<E2 extends Error>(fn: (fail: never) => E2): Success<T>, // If the result is an error, map the error to another error | ||
transformOnSuccess<N>(fn: (data: T) => N): Success<N> // If the result is not an error, map the data in it | ||
}; | ||
type Result<T, E extends Error = ErrorResult> = | ||
| Fail<T, E> | ||
| Ok<T>; | ||
export type Result< | ||
T, E extends Error = Error | ||
> = Failure<E> | Success<T>; | ||
@@ -202,4 +180,6 @@ ``` | ||
```ts | ||
function fail<E extends Error>(error: E): Fail<E>; | ||
function ok<T>(data: T): Ok<T>; | ||
export type ResultIs = { | ||
success<T>(data: T): Success<T>, | ||
failure<E extends Error>(failure: E): Failure<E> | ||
}; | ||
``` | ||
@@ -206,0 +186,0 @@ |
type Ok<T> = import(".").Ok<T>; | ||
type Fail<E extends Error> = import(".").Fail<E>; | ||
type Success<T> = import(".").Success<T>; | ||
type Failure<E extends Error> = import(".").Failure<E>; | ||
type Result<T, E extends Error> = import(".").Result<T, E>; | ||
declare class ResultIs { | ||
static success: <T>(data: T) => Success<T>; | ||
static failure: <E extends Error>(failure: E) => Failure<E>; | ||
} | ||
declare function ok<T>(data: T): Ok<T>; | ||
declare function fail<E extends Error>(error: E): Fail<E> | ||
declare function withResult<T, E extends Error, R>( | ||
@@ -12,2 +14,2 @@ fn: (...args: T[]) => R | ||
...args: T[] | ||
) => R extends Promise<infer u> ? Promise<Result<u, E>> : Result<R, E> | ||
) => R extends Promise<infer u> ? Promise<Result<u, E>> : Result<R, E>; |
@@ -1,12 +0,10 @@ | ||
import { ok, fail } from "."; | ||
import { ResultIs } from "."; | ||
import { withResult } from "./with-result"; | ||
if (typeof window !== "undefined") { | ||
(window as any).ok = ok; | ||
(window as any).fail = fail; | ||
(window as any).ResultIs = ResultIs; | ||
(window as any).withResult = withResult; | ||
} else { | ||
(globalThis as any).ok = ok; | ||
(globalThis as any).fail = fail; | ||
(globalThis as any).ResultIs = ResultIs; | ||
(globalThis as any).withResult = withResult; | ||
} |
export type Fail<E extends Error = Error> = { | ||
export type Failure<E extends Error = Error> = { | ||
data: null, | ||
error: E, | ||
unwrap(): void, // Returns the value, but throws an error if the result is an Error | ||
unwrapOr<D>(defaultValue: D): D, // Returns the value or gives you a default value if it's an error | ||
mapFail<E2 extends Error>(fn: (fail: E) => E2): Fail<E2>, // If the result is an error, map the error to another error | ||
andThen<N>(fn: (data: never) => N): Fail<E> // If the result is not an error, map the data in it | ||
successOrThrow(): void, // Returns the value, but throws an error if the result is an Error | ||
successOrDefault<D>(defaultValue: D): D, // Returns the value or gives you a default value if it's an error | ||
transformOnFailure<E2 extends Error>(fn: (fail: E) => E2): Failure<E2>, // If the result is an error, map the error to another error | ||
transformOnSuccess<N>(fn: (data: never) => N): Failure<E> // If the result is not an error, map the data in it | ||
}; | ||
export type Ok<T> = { | ||
export type Success<T> = { | ||
data: T, | ||
error: null, | ||
unwrap(): T, // Returns the value, but throws an error if the result is an Error | ||
unwrapOr<D>(defaultValue: D): T, // Returns the value or gives you a default value if it's an error | ||
mapFail<E2 extends Error>(fn: (fail: never) => E2): Ok<T>, // If the result is an error, map the error to another error | ||
andThen<N>(fn: (data: T) => N): Ok<N> // If the result is not an error, map the data in it | ||
successOrThrow(): T, // Returns the value, but throws an error if the result is an Error | ||
successOrDefault<D>(defaultValue: D): T, // Returns the value or gives you a default value if it's an error | ||
transformOnFailure<E2 extends Error>(fn: (fail: never) => E2): Success<T>, // If the result is an error, map the error to another error | ||
transformOnSuccess<N>(fn: (data: T) => N): Success<N> // If the result is not an error, map the data in it | ||
}; | ||
@@ -23,43 +23,50 @@ | ||
T, E extends Error = Error | ||
> = Fail<E> | Ok<T>; | ||
> = Failure<E> | Success<T>; | ||
export const fail = <E extends Error>( | ||
const failure = <E extends Error>( | ||
error: E | ||
): Fail<E> => { | ||
return { | ||
data: null, | ||
error: error, | ||
unwrap() { | ||
throw error; | ||
}, | ||
mapFail<E2 extends Error>(fn: (err: E) => E2): Fail<E2> { | ||
return fail<E2>(fn(error)); | ||
}, | ||
unwrapOr<D>(defaultValue: D): D { | ||
return defaultValue; | ||
}, | ||
andThen(): Fail<E> { | ||
return this; | ||
} | ||
}; | ||
}; | ||
export const ok = <T>( | ||
): Failure<E> => ({ | ||
data: null, | ||
error: error, | ||
successOrThrow() { | ||
throw error; | ||
}, | ||
successOrDefault<D>(defaultValue: D): D { | ||
return defaultValue; | ||
}, | ||
transformOnFailure<E2 extends Error>(fn: (err: E) => E2): Failure<E2> { | ||
return failure<E2>(fn(error)); | ||
}, | ||
transformOnSuccess(): Failure<E> { | ||
return this; | ||
} | ||
}); | ||
const success = <T>( | ||
data: T | ||
): Ok<T> => ({ | ||
): Success<T> => ({ | ||
data, | ||
error: null, | ||
unwrap(): T { | ||
successOrThrow(): T { | ||
return data; | ||
}, | ||
mapFail(): Ok<T> { | ||
successOrDefault(): T { | ||
return data; | ||
}, | ||
transformOnFailure(): Success<T> { | ||
return this; | ||
}, | ||
unwrapOr(): T { | ||
return data; | ||
}, | ||
andThen<N>(fn: (data: T) => N): Ok<N> { | ||
return ok(fn(data)); | ||
transformOnSuccess<N>(fn: (data: T) => N): Success<N> { | ||
return success(fn(data)); | ||
} | ||
}); | ||
export type ResultIs = { | ||
success<T>(data: T): Success<T>, | ||
failure<E extends Error>(failure: E): Failure<E> | ||
}; | ||
export const ResultIs: ResultIs = { | ||
success, | ||
failure | ||
}; | ||
@@ -1,2 +0,2 @@ | ||
import { fail, Fail, Result, Ok, ok } from "../src"; | ||
import { ResultIs, Failure, Result, Success } from "../src"; | ||
@@ -11,11 +11,11 @@ | ||
beforeEach(() => { | ||
errorInstance = fail(testError); | ||
errorInstance = ResultIs.failure(testError); | ||
}); | ||
it("should throw error on unwrap for Err type", () => { | ||
expect(() => errorInstance.unwrap()).toThrow(testError); | ||
expect(() => errorInstance.successOrThrow()).toThrow(testError); | ||
}); | ||
it("should return default value on unwrapOr for Err type", () => { | ||
expect(errorInstance.unwrapOr("default")).toBe("default"); | ||
expect(errorInstance.successOrDefault("default")).toBe("default"); | ||
}); | ||
@@ -25,3 +25,3 @@ | ||
const newError = new Error("New error"); | ||
const { error } = errorInstance.mapFail(() => newError); | ||
const { error } = errorInstance.transformOnFailure(() => newError); | ||
if (error) { | ||
@@ -33,3 +33,3 @@ expect(error.message).toBe(newError.message); | ||
it("should return itself on andThen for Err type", () => { | ||
const result = errorInstance.andThen(() => "new value"); | ||
const result = errorInstance.transformOnSuccess(() => "new value"); | ||
expect(JSON.stringify(result)).toEqual(JSON.stringify(errorInstance)); | ||
@@ -40,7 +40,7 @@ }); | ||
describe("Ok type", () => { | ||
let okInstance: Ok<string>; | ||
let okInstance: Success<string>; | ||
const testData = "test data"; | ||
beforeEach(() => { | ||
okInstance = ok(testData); | ||
okInstance = ResultIs.success(testData); | ||
}); | ||
@@ -57,11 +57,11 @@ | ||
it("should return data on unwrap for Ok type", () => { | ||
expect(okInstance.unwrap()).toBe(testData); | ||
expect(okInstance.successOrThrow()).toBe(testData); | ||
}); | ||
it("should return data on unwrapOr for Ok type", () => { | ||
expect(okInstance.unwrapOr("default")).toBe(testData); | ||
expect(okInstance.successOrDefault("default")).toBe(testData); | ||
}); | ||
it("should return itself on mapErr for Ok type", () => { | ||
const result = okInstance.mapFail(() => new Error("New error")); | ||
const result = okInstance.transformOnFailure(() => new Error("New error")); | ||
expect(JSON.stringify(result)).toEqual(JSON.stringify(okInstance)); | ||
@@ -72,4 +72,4 @@ }); | ||
const newValue = "new value"; | ||
const result = okInstance.andThen(() => newValue); | ||
expect(JSON.stringify(result)).toEqual(JSON.stringify(ok(newValue))); | ||
const result = okInstance.transformOnSuccess(() => newValue); | ||
expect(JSON.stringify(result)).toEqual(JSON.stringify(ResultIs.success(newValue))); | ||
expect(result.data).toBe(newValue); | ||
@@ -76,0 +76,0 @@ }); |
@@ -1,2 +0,2 @@ | ||
import { fail, ok, Result, Ok, Fail } from "./index"; | ||
import { ResultIs, Result, Success, Failure } from "./index"; | ||
@@ -17,9 +17,9 @@ export const isPromise = <T>(value: T | Promise<T>): value is Promise<T> => | ||
return data | ||
.then(value => ok(value) as Ok<typeof value>) | ||
.catch(e => fail(e) as Fail<E>) as (R extends Promise<infer u> ? Promise<Result<u, E>> : Result<R, E>); | ||
.then(value => ResultIs.success(value) as Success<typeof value>) | ||
.catch(e => ResultIs.failure(e) as Failure<E>) as (R extends Promise<infer u> ? Promise<Result<u, E>> : Result<R, E>); | ||
} | ||
return ok(data) as any; | ||
return ResultIs.success(data) as any; | ||
} catch (error) { | ||
const e = error instanceof Error ? error : new Error("Unknown error"); | ||
return fail(e) as any; | ||
return ResultIs.failure(e) as any; | ||
} | ||
@@ -26,0 +26,0 @@ }; |
// Assuming a jest-like syntax for demonstration purposes. | ||
import { ok, Result } from "../src"; | ||
import { ResultIs, Result } from "../src"; | ||
import { withResult, isPromise } from "./with-result"; | ||
@@ -23,3 +23,3 @@ | ||
const wrapped = withResult(func); | ||
expect(JSON.stringify(wrapped(1))).toStrictEqual(JSON.stringify(ok(2))); | ||
expect(JSON.stringify(wrapped(1))).toStrictEqual(JSON.stringify(ResultIs.success(2))); | ||
}); | ||
@@ -42,3 +42,3 @@ | ||
const result = await wrapped(1); | ||
expect(JSON.stringify(result)).toEqual(JSON.stringify(ok(2))); | ||
expect(JSON.stringify(result)).toEqual(JSON.stringify(ResultIs.success(2))); | ||
}); | ||
@@ -45,0 +45,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
27149
31
597
197