From b4ba21928c7c2d3f795b8650297aa7f36de66e07 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 28 Apr 2025 13:37:21 -0700 Subject: [PATCH 1/9] made Error type optionally generic --- src/index.ts | 53 +++++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/index.ts b/src/index.ts index d769bd3..ead157b 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,7 +46,7 @@ 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. */ @@ -55,24 +57,26 @@ export class Res extends Array { /** * 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 { + static ok(value: G): TryResultOk { return Res.from([value, undefined]) } - static err(exception: unknown): TryResultError { + 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 +92,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 +106,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 +125,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 +155,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})` } } @@ -217,12 +220,12 @@ export class Try { * return jsonData * ``` */ - static catch(fn: () => never): TryResultError - static catch(fn: () => Promise): Promise> - static catch(fn: () => T): TryResult - static catch( + static catch(fn: () => never): TryResultError + static catch(fn: () => Promise): Promise> + static catch(fn: () => T): TryResult + static catch( fn: () => T | Promise - ): TryResult | Promise> { + ): TryResult | Promise> { try { const output = fn() if (output instanceof Promise) { From 618ecfc14c8e6424a5499bc89418613ee1917c64 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 28 Apr 2025 20:38:39 -0700 Subject: [PATCH 2/9] updated default error param --- src/index.ts | 22 +++++++++++----------- test/index.test.ts | 19 ++++++++++++++++++- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/index.ts b/src/index.ts index ead157b..00be791 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ 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 @@ -46,7 +46,7 @@ export type TryResult = * 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. */ @@ -57,19 +57,19 @@ export class Res extends Array { /** * 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 { + static ok(value: Val): TryResultOk { return Res.from([value, undefined]) } - static err(exception: unknown): TryResultError { + static err(exception: unknown): TryResultError { return Res.from([undefined, Res.toError(exception)]) } diff --git a/test/index.test.ts b/test/index.test.ts index c2d01c8..d06e280 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -454,7 +454,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 +470,20 @@ 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 INVALID_CHARS = /[A-Z]/g + 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/") + +}) \ No newline at end of file From cc327f1a55b7e42bc4c68a33c227479eab25a8a5 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 28 Apr 2025 20:53:49 -0700 Subject: [PATCH 3/9] updated specified tests --- test/index.test.ts | 52 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/test/index.test.ts b/test/index.test.ts index d06e280..416d698 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -471,19 +471,49 @@ test('Can create result tuple with Res.ok', () => { expect(edgeCase1.unwrap()).toBeUndefined() }) - test('Can handle multiple statements', () => { - - const userInput = "" - const INVALID_CHARS = /[A-Z]/g - const FALLBACK_URL = new URL("https://example.com") + 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)) + .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' + } + } - expect(url.href).toBe("https://example.com/") + function canThrow(): string | never { + if (Math.random() < 1.0) { + throw new CustomError() + } else { + return 'hello' + } + } -}) \ No newline at end of file + 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)') +}) From b3ed7ade98972db71834072627a09dcf2f26f611 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 28 Apr 2025 21:39:49 -0700 Subject: [PATCH 4/9] added support for excpetions --- src/index.ts | 44 ++++++++++++++++++++++++++++++-------------- test/index.test.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index 00be791..c5419d8 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, E] +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,7 @@ export type TryResultError = Res & { /** * Result tuple returned from calling `Try.catch(fn)` */ -export type TryResult = +export type TryResult = | TryResultOk | TryResultError @@ -46,7 +46,7 @@ export type TryResult = * 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. */ @@ -57,19 +57,31 @@ export class Res extends Array { /** * Helper methods for instantiating via a tuple. */ - static from(tuple: ErrorTuple): TryResultError - static from(tuple: OkTuple): TryResultOk - static from( + static from( + tuple: ErrorTuple + ): TryResultError + static from( + tuple: OkTuple + ): TryResultOk + static from( tuple: OkTuple | ErrorTuple ): TryResult { return new Res(tuple) as TryResult } + /** + * 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)]) } @@ -220,11 +232,15 @@ 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 + 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() diff --git a/test/index.test.ts b/test/index.test.ts index 416d698..83a33e2 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') @@ -517,3 +518,35 @@ test('Can specify specific type of Error to expect', () => { 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)') +}) From ef99c0124343967434ff63b4952a633458458c8c Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 28 Apr 2025 21:47:48 -0700 Subject: [PATCH 5/9] added init method --- src/index.ts | 19 +++++++++++++++++++ test/index.test.ts | 12 ++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/index.ts b/src/index.ts index c5419d8..d2a7f39 100644 --- a/src/index.ts +++ b/src/index.ts @@ -179,6 +179,13 @@ export class Res extends Array { } } +/** + * Special Types + */ +type GenericConstructor = { + new (...args: Args): T +} + /** * ## Try * @@ -255,6 +262,18 @@ export class Try { return Res.err(e) } } + + /** + * Utility for initializing a class instance + */ + static init( + ctor: GenericConstructor, + ...args: A + ) { + return Try.catch, Err>(() => { + return new ctor(...args) + }) + } } /** diff --git a/test/index.test.ts b/test/index.test.ts index 83a33e2..01414d1 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -550,3 +550,15 @@ test('Can call Try.expect with custom error type as first generic argument', () 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()) + if (result.isOk()) { + expect(result.value.href).toEqual('https://asleepace.com/') + } + expect(result.value instanceof URL) +}) From e2e08adb9199d0eebe8936597359c79cd8526771 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 28 Apr 2025 21:53:39 -0700 Subject: [PATCH 6/9] added Try.init method --- src/index.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index d2a7f39..06f3529 100644 --- a/src/index.ts +++ b/src/index.ts @@ -264,13 +264,23 @@ export class Try { } /** - * Utility for initializing a class instance + * 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 + * ``` + * */ static init( - ctor: GenericConstructor, + ctor: new (...args: A) => T, ...args: A ) { - return Try.catch, Err>(() => { + return Try.catch(() => { return new ctor(...args) }) } From f253ff97cec7acf6a6448f9d5a519029b78e2f81 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 28 Apr 2025 21:54:20 -0700 Subject: [PATCH 7/9] added notice --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 06f3529..e4014b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -274,6 +274,7 @@ export class Try { * * if (result.isOK()) return result.hostname * ``` + * @note this is a beta feature and subject to change. * */ static init( From 320edbbd6d265bd76b4da5c499857dfa81e81f03 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 28 Apr 2025 21:56:55 -0700 Subject: [PATCH 8/9] added Try.init test --- test/index.test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/index.test.ts b/test/index.test.ts index 01414d1..9c41c8d 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -556,9 +556,20 @@ test('Can call Try.expect with custom error type as first generic argument', () */ test('Can call Try.init() to instantiate an instance of a class', () => { const result = Try.init(URL, 'https://asleepace.com/') - expect(result.isOk()) + expect(result.isOk()).toBe(true) if (result.isOk()) { expect(result.value.href).toEqual('https://asleepace.com/') } - expect(result.value instanceof URL) + 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) + console.log(result.value) + expect(result.isErr()).toBe(true) + expect(result.value instanceof URL).toBe(false) }) From fca1b76911d9aedffa5e9a6d26d651be1361639c Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 28 Apr 2025 22:36:22 -0700 Subject: [PATCH 9/9] allowing custom error handler --- src/index.ts | 27 ++++++++++++++++++--------- test/index.test.ts | 25 ++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index e4014b7..5e17130 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,8 +50,16 @@ 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 } /** @@ -179,13 +187,6 @@ export class Res extends Array { } } -/** - * Special Types - */ -type GenericConstructor = { - new (...args: Args): T -} - /** * ## Try * @@ -220,6 +221,14 @@ type GenericConstructor = { * */ 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 diff --git a/test/index.test.ts b/test/index.test.ts index 9c41c8d..83c188f 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -569,7 +569,30 @@ test('Can call Try.init() to instantiate an instance of a class', () => { test('Can call Try.init() to instantiate an instance of a class', () => { // @ts-ignore const result = Try.init(URL, 'https://asleepace.com/', 123, 435) - console.log(result.value) 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) +})