diff --git a/src/index.ts b/src/index.ts index d769bd3..5e17130 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,12 +6,12 @@ export type OkTuple = [T, undefined] /** * Primitive result tuple which contains an error. */ -export type ErrorTuple = [undefined, Error] +export type ErrorTuple = [undefined, E] /** * Result tuple which contains a value. */ -export type TryResultOk = Res & { +export type TryResultOk = Res & { 0: T 1: undefined value: T @@ -23,7 +23,7 @@ export type TryResultOk = Res & { /** * Result tuple which contains an error. */ -export type TryResultError = Res & { +export type TryResultError = Res & { 0: undefined 1: Error value: undefined @@ -35,7 +35,9 @@ export type TryResultError = Res & { /** * Result tuple returned from calling `Try.catch(fn)` */ -export type TryResult = TryResultOk | TryResultError +export type TryResult = + | TryResultOk + | TryResultError /** * ## Res @@ -44,35 +46,57 @@ export type TryResult = TryResultOk | TryResultError * several convenience methods for accessing data and checking types. * */ -export class Res extends Array { +export class Res extends Array { /** * Helper to convert a caught exception to an Error instance. */ - static toError = (exception: unknown): Error => { - return exception instanceof Error ? exception : new Error(String(exception)) + static toError = (exception: unknown) => { + return exception instanceof Error + ? exception + : (new Error(String(exception)) as Err) + } + + static isError = ( + exception: unknown + ): exception is Err => { + return exception instanceof Error } /** * Helper methods for instantiating via a tuple. */ - static from(tuple: ErrorTuple): TryResultError - static from(tuple: OkTuple): TryResultOk - static from(tuple: OkTuple | ErrorTuple): TryResult { - return new Res(tuple) as TryResult + static from( + tuple: ErrorTuple + ): TryResultError + static from( + tuple: OkTuple + ): TryResultOk + static from( + tuple: OkTuple | ErrorTuple + ): TryResult { + return new Res(tuple) as TryResult } - static ok(value: G): TryResultOk { + /** + * Instantiate a new result tuple with a value. + */ + static ok(value: Val): TryResultOk { return Res.from([value, undefined]) } - static err(exception: unknown): TryResultError { + /** + * Instantiate a new result tuple with an error. + */ + static err( + exception: unknown + ): TryResultError { return Res.from([undefined, Res.toError(exception)]) } declare 0: T | undefined - declare 1: Error | undefined + declare 1: E | undefined - constructor([value, error]: OkTuple | ErrorTuple) { + constructor([value, error]: OkTuple | ErrorTuple) { super(2) this[0] = value this[1] = error @@ -88,7 +112,7 @@ export class Res extends Array { /** * Getter which returns the error in the result tuple. */ - get error(): Error | undefined { + get error(): E | undefined { return this[1] } @@ -102,14 +126,14 @@ export class Res extends Array { /** * Returns true if this is the `TryResultOk` variant. */ - public isOk(): this is TryResultOk { + public isOk(): this is TryResultOk { return this.error === undefined } /** * Returns true if this is the `TryResultError` variant. */ - public isErr(): this is TryResultError { + public isErr(): this is TryResultError { return this.error !== undefined } @@ -121,9 +145,8 @@ export class Res extends Array { */ public unwrap(): T | never { if (this.isOk()) return this.value - throw new Error( - `Failed to unwrap result with error: ${this.error?.message}` - ) + console.warn(`Failed to unwrap result with error: ${this.error}`) + throw this.error } /** @@ -152,7 +175,7 @@ export class Res extends Array { if (this.ok) { return `Result.Ok(${String(this.value)})` } else { - return `Result.Error(${this.error?.message})` + return `Result.Error(${this.error})` } } @@ -198,6 +221,14 @@ export class Res extends Array { * */ export class Try { + /** + * Allows overriding some of the default properties such as how to handle exceptions + * and how tht result class should look. + */ + static onException(handler: (exception: unknown) => E) { + Res.toError = handler + } + /** * Simple error handling utility which will invoke the provided function and * catch any thrown errors, the result of the function execution will then be @@ -217,12 +248,16 @@ export class Try { * return jsonData * ``` */ - static catch(fn: () => never): TryResultError - static catch(fn: () => Promise): Promise> - static catch(fn: () => T): TryResult - static catch( - fn: () => T | Promise - ): TryResult | Promise> { + static catch( + fn: () => never + ): TryResultError + static catch( + fn: () => Promise + ): Promise> + static catch(fn: () => T): TryResult + static catch( + fn: () => T | never | Promise + ): TryResult | Promise> { try { const output = fn() if (output instanceof Promise) { @@ -236,6 +271,29 @@ export class Try { return Res.err(e) } } + + /** + * Utility for initializing a class instance with the given parameters + * and catching any exceptions thrown. Will return a result tuple of + * either the class instance or error thrown. + * + * ```ts + * // example instantiating a new URL instance + * const result = Try.init(URL, "https://www.typescriptlang.org/") + * + * if (result.isOK()) return result.hostname + * ``` + * @note this is a beta feature and subject to change. + * + */ + static init( + ctor: new (...args: A) => T, + ...args: A + ) { + return Try.catch(() => { + return new ctor(...args) + }) + } } /** diff --git a/test/index.test.ts b/test/index.test.ts index c2d01c8..83c188f 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -21,6 +21,7 @@ test('Can use vet shorthand utility with or chaining', () => { expect(link instanceof URL).toBe(true) }) +// Result can call result helper methods isOk and isErr test('Res can call the isOk() and isErr() methods', async () => { let resultError = Try.catch(() => { throw new Error('alwaysThrows') @@ -454,7 +455,7 @@ test('Can call toString on Res class', () => { const result2 = Try.catch(() => { throw new Error('456') }) - expect(result2.toString()).toBe('Result.Error(456)') + expect(result2.toString()).toBe('Result.Error(Error: 456)') }) test('Can create result tuple with Res.ok', () => { @@ -470,3 +471,128 @@ test('Can create result tuple with Res.ok', () => { expect(edgeCase1.isErr()).toBe(false) expect(edgeCase1.unwrap()).toBeUndefined() }) + +test('Can handle multiple statements', () => { + const userInput = '' + const FALLBACK_URL = new URL('https://example.com') + + const url = Try.catch(() => new URL(userInput)) + .or(() => new URL(`https://${userInput}`)) + .or(() => new URL(`https://${userInput.replace('http://', '')}`)) + .or(() => new URL(`https://${userInput.split('://')[1]!.trim()}`)) + .unwrapOr(new URL(FALLBACK_URL)) + + expect(url.href).toBe('https://example.com/') +}) + +/** + * Check if caller can specify customer error type. + * - Create Error subclass + * - Create Fn which can throw custom Error + * - Check if the return types are correct + * - Can access custom properties + */ +test('Can specify specific type of Error to expect', () => { + class CustomError extends Error { + public name = 'MyCustomError' + public code = 117 + get [Symbol.toStringTag]() { + console.log('toStringTag called!') + return 'CustomError' + } + } + + function canThrow(): string | never { + if (Math.random() < 1.0) { + throw new CustomError() + } else { + return 'hello' + } + } + + const result = Try.catch(canThrow) + expect(result.ok).toBe(false) + expect(result.isOk()).toBe(false) + expect(result.isErr()).toBe(true) + expect(result.error instanceof CustomError).toBe(true) + expect(result.error!.code).toBe(117) + expect(result.toString()).toBe('Result.Error(MyCustomError)') +}) + +/** + * Check if `Try.expect()` works as expected and contains proper types. + */ +test('Can call Try.expect with custom error type as first generic argument', () => { + class CustomError extends Error { + public name = 'MyCustomError' + public code = 117 + get [Symbol.toStringTag]() { + console.log('toStringTag called!') + return 'CustomError' + } + } + + class CustomObject {} + + function canThrow(): CustomObject | never { + if (Math.random() < 1.0) { + throw new CustomError() + } else { + return new CustomObject() + } + } + + const result = Try.catch(canThrow) + expect(result.ok).toBe(false) + expect(result.isOk()).toBe(false) + expect(result.isErr()).toBe(true) + expect(result.error instanceof CustomError).toBe(true) + expect(result.error!.code).toBe(117) + expect(result.toString()).toBe('Result.Error(MyCustomError)') +}) + +/** + * Check if `Try.init(URL, urlString)` works as expected. + */ +test('Can call Try.init() to instantiate an instance of a class', () => { + const result = Try.init(URL, 'https://asleepace.com/') + expect(result.isOk()).toBe(true) + if (result.isOk()) { + expect(result.value.href).toEqual('https://asleepace.com/') + } + expect(result.value instanceof URL).toBe(true) +}) + +/** + * Check if `Try.init(URL)` works with invalid parameters. + */ +test('Can call Try.init() to instantiate an instance of a class', () => { + // @ts-ignore + const result = Try.init(URL, 'https://asleepace.com/', 123, 435) + expect(result.isErr()).toBe(true) + expect(result.value instanceof URL).toBe(false) +}) + +/** + * Can set custom configuration for handling exceptions. + */ +test('Can set custom configuration for handling errors', () => { + class MyCustomError extends Error { + static isCustom = true + constructor(message: unknown) { + super(String(message)) + } + } + + Try.onException((exception) => { + return new MyCustomError(exception) + }) + + const result = Try.catch(() => { + throw new Error('always') + }) + + expect(result.isOk()).toBe(false) + expect(result.isErr()).toBe(true) + expect(result.error instanceof MyCustomError).toBe(true) +})