Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 86 additions & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ export type OkTuple<T> = [T, undefined]
/**
* Primitive result tuple which contains an error.
*/
export type ErrorTuple = [undefined, Error]
export type ErrorTuple<E extends Error = Error> = [undefined, E]

/**
* Result tuple which contains a value.
*/
export type TryResultOk<T> = Res<T> & {
export type TryResultOk<T, E extends Error = Error> = Res<T, never> & {
0: T
1: undefined
value: T
Expand All @@ -23,7 +23,7 @@ export type TryResultOk<T> = Res<T> & {
/**
* Result tuple which contains an error.
*/
export type TryResultError = Res<never> & {
export type TryResultError<T, E extends Error = Error> = Res<never, E> & {
0: undefined
1: Error
value: undefined
Expand All @@ -35,7 +35,9 @@ export type TryResultError = Res<never> & {
/**
* Result tuple returned from calling `Try.catch(fn)`
*/
export type TryResult<T> = TryResultOk<T> | TryResultError
export type TryResult<T, E extends Error = Error> =
| TryResultOk<T, never>
| TryResultError<never, E>

/**
* ## Res
Expand All @@ -44,35 +46,57 @@ export type TryResult<T> = TryResultOk<T> | TryResultError
* several convenience methods for accessing data and checking types.
*
*/
export class Res<T> extends Array {
export class Res<T, E extends Error = Error> 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 = <Err extends Error = Error>(exception: unknown) => {
return exception instanceof Error
? exception
: (new Error(String(exception)) as Err)
}

static isError = <Err extends Error = Error>(
exception: unknown
): exception is Err => {
return exception instanceof Error
}

/**
* Helper methods for instantiating via a tuple.
*/
static from<G>(tuple: ErrorTuple): TryResultError
static from<G>(tuple: OkTuple<G>): TryResultOk<G>
static from<G>(tuple: OkTuple<G> | ErrorTuple): TryResult<G> {
return new Res(tuple) as TryResult<G>
static from<Val, Err extends Error = Error>(
tuple: ErrorTuple
): TryResultError<never, Err>
static from<Val, Err extends Error = Error>(
tuple: OkTuple<Val>
): TryResultOk<Val, never>
static from<Val, Err extends Error = Error>(
tuple: OkTuple<Val> | ErrorTuple
): TryResult<Val, Err> {
return new Res(tuple) as TryResult<Val, Err>
}

static ok<G>(value: G): TryResultOk<G> {
/**
* Instantiate a new result tuple with a value.
*/
static ok<Val>(value: Val): TryResultOk<Val, never> {
return Res.from([value, undefined])
}

static err<G>(exception: unknown): TryResultError {
/**
* Instantiate a new result tuple with an error.
*/
static err<Err extends Error = Error>(
exception: unknown
): TryResultError<never, Err> {
return Res.from([undefined, Res.toError(exception)])
}

declare 0: T | undefined
declare 1: Error | undefined
declare 1: E | undefined

constructor([value, error]: OkTuple<T> | ErrorTuple) {
constructor([value, error]: OkTuple<T> | ErrorTuple<E>) {
super(2)
this[0] = value
this[1] = error
Expand All @@ -88,7 +112,7 @@ export class Res<T> extends Array {
/**
* Getter which returns the error in the result tuple.
*/
get error(): Error | undefined {
get error(): E | undefined {
return this[1]
}

Expand All @@ -102,14 +126,14 @@ export class Res<T> extends Array {
/**
* Returns true if this is the `TryResultOk<T>` variant.
*/
public isOk(): this is TryResultOk<T> {
public isOk(): this is TryResultOk<T, never> {
return this.error === undefined
}

/**
* Returns true if this is the `TryResultError` variant.
*/
public isErr(): this is TryResultError {
public isErr(): this is TryResultError<never, E> {
return this.error !== undefined
}

Expand All @@ -121,9 +145,8 @@ export class Res<T> 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
}

/**
Expand Down Expand Up @@ -152,7 +175,7 @@ export class Res<T> extends Array {
if (this.ok) {
return `Result.Ok(${String(this.value)})`
} else {
return `Result.Error(${this.error?.message})`
return `Result.Error(${this.error})`
}
}

Expand Down Expand Up @@ -198,6 +221,14 @@ export class Res<T> 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<E extends Error>(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
Expand All @@ -217,12 +248,16 @@ export class Try {
* return jsonData
* ```
*/
static catch<T>(fn: () => never): TryResultError
static catch<T>(fn: () => Promise<T>): Promise<TryResult<T>>
static catch<T>(fn: () => T): TryResult<T>
static catch<T>(
fn: () => T | Promise<T>
): TryResult<T> | Promise<TryResult<T>> {
static catch<T, Err extends Error = Error>(
fn: () => never
): TryResultError<never, Err>
static catch<T, Err extends Error = Error>(
fn: () => Promise<T>
): Promise<TryResult<T, Err>>
static catch<T, Err extends Error = Error>(fn: () => T): TryResult<T, Err>
static catch<T, Err extends Error = Error>(
fn: () => T | never | Promise<T>
): TryResult<T, Err> | Promise<TryResult<T, Err>> {
try {
const output = fn()
if (output instanceof Promise) {
Expand All @@ -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<T, A extends any[] = any[], Err extends Error = Error>(
ctor: new (...args: A) => T,
...args: A
) {
return Try.catch<T, Err>(() => {
return new ctor(...args)
})
}
}

/**
Expand Down
128 changes: 127 additions & 1 deletion test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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<string, CustomError>(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<CustomObject, CustomError>(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)
})