Skip to content

Commit 9ebf93f

Browse files
committed
i need to figure out my formatter.
1 parent 5ef3a01 commit 9ebf93f

5 files changed

Lines changed: 273 additions & 4 deletions

File tree

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { Result, ok, Ok, err, Err, fromThrowable, safeTry } from './result'
1+
export { Result, ok, Ok, err, Err, TaggedError, fromThrowable, safeTry } from './result'
22
export {
33
ResultAsync,
44
okAsync,

src/result-async.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
MembersToUnion,
88
} from './result'
99

10-
import { Err, Ok, Result } from './'
10+
import { Err, Ok, Result, TaggedError } from './'
1111
import {
1212
combineResultAsyncList,
1313
combineResultAsyncListWithAllErrors,
@@ -199,6 +199,34 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
199199
)
200200
}
201201

202+
catchTags<Handlers extends CatchTagsHandlersFor<E>>(
203+
handlers: Handlers,
204+
): ResultAsync<T | CatchTagsHandlerOkTypes<Handlers>, E | CatchTagsHandlerErrTypes<Handlers>>
205+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
206+
catchTags(handlers: any): any {
207+
return new ResultAsync(
208+
this._promise.then(async (res: Result<T, E>) => {
209+
if (res.isOk()) {
210+
return new Ok(res.value)
211+
}
212+
213+
if (res.error instanceof TaggedError) {
214+
const handler = handlers[res.error._tag] as
215+
| ((
216+
error: TaggedErrorUnion,
217+
) => Result<unknown, unknown> | ResultAsync<unknown, unknown>)
218+
| undefined
219+
if (handler !== undefined) {
220+
const handledResult = handler(res.error)
221+
return handledResult instanceof ResultAsync ? handledResult._promise : handledResult
222+
}
223+
}
224+
225+
return new Err(res.error)
226+
}),
227+
)
228+
}
229+
202230
match<A, B = A>(ok: (t: T) => A, _err: (e: E) => B): Promise<A | B> {
203231
return this._promise.then((res) => res.match(ok, _err))
204232
}
@@ -336,3 +364,34 @@ type TraverseWithAllErrorsAsync<T, Depth extends number = 5> = TraverseAsync<
336364

337365
// Converts a reaodnly array into a writable array
338366
type Writable<T> = T extends ReadonlyArray<unknown> ? [...T] : T
367+
368+
type TaggedErrorUnion = TaggedError<string>
369+
type CatchTagsHandlers = {
370+
[K in string]?: (
371+
error: TaggedErrorUnion,
372+
) => Result<unknown, unknown> | ResultAsync<unknown, unknown>
373+
}
374+
type CatchTagsHandlersFor<E> = Partial<
375+
{
376+
[K in Extract<Extract<E, TaggedErrorUnion>['_tag'], string>]: (
377+
error: Extract<Extract<E, TaggedErrorUnion>, { _tag: K }>,
378+
) => Result<unknown, unknown> | ResultAsync<unknown, unknown>
379+
}
380+
>
381+
type CatchTagsHandlerResultUnion<Handlers> = {
382+
[K in keyof Handlers]: Handlers[K] extends (...args: readonly unknown[]) => infer R ? R : never
383+
}[keyof Handlers]
384+
type CatchTagsHandlerResultSyncUnion<Handlers> = Extract<
385+
CatchTagsHandlerResultUnion<Handlers>,
386+
Result<unknown, unknown>
387+
>
388+
type CatchTagsHandlerResultAsyncUnion<Handlers> = Extract<
389+
CatchTagsHandlerResultUnion<Handlers>,
390+
ResultAsync<unknown, unknown>
391+
>
392+
type CatchTagsHandlerOkTypes<Handlers> =
393+
| InferOkTypes<CatchTagsHandlerResultSyncUnion<Handlers>>
394+
| InferAsyncOkTypes<CatchTagsHandlerResultAsyncUnion<Handlers>>
395+
type CatchTagsHandlerErrTypes<Handlers> =
396+
| InferErrTypes<CatchTagsHandlerResultSyncUnion<Handlers>>
397+
| InferAsyncErrTypes<CatchTagsHandlerResultAsyncUnion<Handlers>>

src/result.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,31 @@ export namespace Result {
6161

6262
export type Result<T, E> = Ok<T, E> | Err<T, E>
6363

64+
type TaggedErrorUnion = TaggedError<string>
65+
type CatchTagsHandlers = {
66+
[K in string]?: (error: TaggedErrorUnion) => Result<unknown, unknown>
67+
}
68+
type CatchTagsHandlersFor<E> = Partial<
69+
{
70+
[K in Extract<Extract<E, TaggedErrorUnion>['_tag'], string>]: (
71+
error: Extract<Extract<E, TaggedErrorUnion>, { _tag: K }>,
72+
) => Result<unknown, unknown>
73+
}
74+
>
75+
type CatchTagsHandlerResultUnion<Handlers> = {
76+
[K in keyof Handlers]: Handlers[K] extends (...args: readonly unknown[]) => infer R ? R : never
77+
}[keyof Handlers]
78+
79+
export class TaggedError<Tag extends string = string> extends Error {
80+
readonly _tag: Tag
81+
82+
constructor(tag: Tag, message?: string) {
83+
super(message)
84+
this._tag = tag
85+
this.name = this.constructor.name
86+
}
87+
}
88+
6489
export function ok<T, E = never>(value: T): Ok<T, E>
6590
export function ok<T extends void = void, E = never>(value: void): Ok<void, E>
6691
export function ok<T, E = never>(value: T): Ok<T, E> {
@@ -232,6 +257,16 @@ interface IResult<T, E> {
232257
): Result<InferOkTypes<R> | T, InferErrTypes<R>>
233258
orElse<U, A>(f: (e: E) => Result<U, A>): Result<U | T, A>
234259

260+
/**
261+
* Catches tagged errors by matching against the `_tag` property.
262+
*/
263+
catchTags<Handlers extends CatchTagsHandlersFor<E>>(
264+
handlers: Handlers,
265+
): Result<
266+
T | InferOkTypes<CatchTagsHandlerResultUnion<Handlers>>,
267+
E | InferErrTypes<CatchTagsHandlerResultUnion<Handlers>>
268+
>
269+
235270
/**
236271
* Similar to `map` Except you must return a new `Result`.
237272
*
@@ -367,6 +402,15 @@ export class Ok<T, E> implements IResult<T, E> {
367402
return ok(this.value)
368403
}
369404

405+
catchTags<Handlers extends CatchTagsHandlersFor<E>>(
406+
_handlers: Handlers,
407+
): Result<
408+
T | InferOkTypes<CatchTagsHandlerResultUnion<Handlers>>,
409+
E | InferErrTypes<CatchTagsHandlerResultUnion<Handlers>>
410+
> {
411+
return ok(this.value)
412+
}
413+
370414
asyncAndThen<U, F>(f: (t: T) => ResultAsync<U, F>): ResultAsync<U, E | F> {
371415
return f(this.value)
372416
}
@@ -396,8 +440,10 @@ export class Ok<T, E> implements IResult<T, E> {
396440

397441
safeUnwrap(): Generator<Err<never, E>, T> {
398442
const value = this.value
399-
/* eslint-disable-next-line require-yield */
400443
return (function* () {
444+
for (const _err of [] as Err<never, E>[]) {
445+
yield _err
446+
}
401447
return value
402448
})()
403449
}
@@ -410,8 +456,10 @@ export class Ok<T, E> implements IResult<T, E> {
410456
throw createNeverThrowError('Called `_unsafeUnwrapErr` on an Ok', this, config)
411457
}
412458

413-
// eslint-disable-next-line @typescript-eslint/no-this-alias, require-yield
414459
*[Symbol.iterator](): Generator<Err<never, E>, T> {
460+
for (const _err of [] as Err<never, E>[]) {
461+
yield _err
462+
}
415463
return this.value
416464
}
417465
}
@@ -471,6 +519,31 @@ export class Err<T, E> implements IResult<T, E> {
471519
return f(this.error)
472520
}
473521

522+
catchTags<Handlers extends CatchTagsHandlersFor<E>>(
523+
handlers: Handlers,
524+
): Result<
525+
T | InferOkTypes<CatchTagsHandlerResultUnion<Handlers>>,
526+
E | InferErrTypes<CatchTagsHandlerResultUnion<Handlers>>
527+
> {
528+
if (this.error instanceof TaggedError) {
529+
const keyedHandlers = handlers as CatchTagsHandlers
530+
const handler = keyedHandlers[this.error._tag] as
531+
| ((error: TaggedErrorUnion) => Result<unknown, unknown>)
532+
| undefined
533+
if (handler !== undefined) {
534+
return handler(this.error as TaggedErrorUnion) as Result<
535+
T | InferOkTypes<CatchTagsHandlerResultUnion<Handlers>>,
536+
E | InferErrTypes<CatchTagsHandlerResultUnion<Handlers>>
537+
>
538+
}
539+
}
540+
541+
return err(this.error) as Result<
542+
T | InferOkTypes<CatchTagsHandlerResultUnion<Handlers>>,
543+
E | InferErrTypes<CatchTagsHandlerResultUnion<Handlers>>
544+
>
545+
}
546+
474547
// eslint-disable-next-line @typescript-eslint/no-unused-vars
475548
asyncAndThen<U, F>(_f: (t: T) => ResultAsync<U, F>): ResultAsync<U, E | F> {
476549
return errAsync<U, E>(this.error)

tests/index.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
okAsync,
1414
Result,
1515
ResultAsync,
16+
TaggedError,
1617
} from '../src'
1718

1819
import { vitest, describe, expect, it } from 'vitest'
@@ -475,6 +476,71 @@ describe('Result.Err', () => {
475476
})
476477
})
477478

479+
describe('TaggedError', () => {
480+
class ParseError extends TaggedError<'ParseError'> {
481+
constructor(public readonly input: string) {
482+
super('ParseError', `Unable to parse: ${input}`)
483+
}
484+
}
485+
486+
class ValidationError extends TaggedError<'ValidationError'> {
487+
constructor(public readonly reason: string) {
488+
super('ValidationError', `Validation failed: ${reason}`)
489+
}
490+
}
491+
492+
it('catches matching tagged errors on Result', () => {
493+
const result = err<number, ParseError | ValidationError>(new ParseError('abc')).catchTags({
494+
ParseError: (_error) => ok(3),
495+
})
496+
497+
expect(result.isOk()).toBe(true)
498+
expect(result._unsafeUnwrap()).toBe(3)
499+
})
500+
501+
it('passes through when no matching handler is present', () => {
502+
const source = err<number, ParseError | ValidationError>(new ValidationError('too short'))
503+
504+
const result = source.catchTags({
505+
ParseError: (_error) => ok(123),
506+
})
507+
508+
expect(result.isErr()).toBe(true)
509+
expect(result._unsafeUnwrapErr()).toBe(source._unsafeUnwrapErr())
510+
})
511+
512+
it('does not catch non-tagged errors', () => {
513+
const originalError = new Error('plain error')
514+
const result = err<number, Error>(originalError).catchTags({
515+
ParseError: (_error: never) => ok(0),
516+
})
517+
518+
expect(result.isErr()).toBe(true)
519+
expect(result._unsafeUnwrapErr()).toBe(originalError)
520+
})
521+
522+
it('catches matching tagged errors on ResultAsync', async () => {
523+
const result = await errAsync<number, ParseError | ValidationError>(
524+
new ValidationError('missing field'),
525+
).catchTags({
526+
ValidationError: (_error) => okAsync('missing field'.length),
527+
})
528+
529+
expect(result.isOk()).toBe(true)
530+
expect(result._unsafeUnwrap()).toBe('missing field'.length)
531+
})
532+
533+
it('passes through non-matching tags on ResultAsync', async () => {
534+
const sourceError = new ValidationError('missing field')
535+
const result = await errAsync<number, ParseError | ValidationError>(sourceError).catchTags({
536+
ParseError: (_error) => okAsync(1),
537+
})
538+
539+
expect(result.isErr()).toBe(true)
540+
expect(result._unsafeUnwrapErr()).toBe(sourceError)
541+
})
542+
})
543+
478544
describe('Result.fromThrowable', () => {
479545
it('Creates a function that returns an OK result when the inner function does not throw', () => {
480546
const hello = (): string => 'hello'

tests/typecheck-tests.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
okAsync,
1313
Result,
1414
ResultAsync,
15+
TaggedError,
1516
} from '../src'
1617
import { safeTry, Transpose } from '../src/result'
1718
import { type N, Test } from 'ts-toolbelt'
@@ -393,6 +394,41 @@ type CreateTuple<L, V = string> =
393394
});
394395
});
395396

397+
(function describe(_ = 'catchTags') {
398+
class ParseError extends TaggedError<'ParseError'> {
399+
constructor(readonly input: string) {
400+
super('ParseError', input)
401+
}
402+
}
403+
404+
class ValidationError extends TaggedError<'ValidationError'> {
405+
constructor(readonly reason: string) {
406+
super('ValidationError', reason)
407+
}
408+
}
409+
410+
(function it(_ = 'suggests keys based on tagged errors in Result error union') {
411+
type SourceError = ParseError | ValidationError | Error
412+
type Expectation = Result<number | string, SourceError | Error>
413+
414+
const source: Result<number, SourceError> = err(new ParseError('raw-input'))
415+
const result: Expectation = source.catchTags({
416+
ParseError: (_error) => ok('raw-input'),
417+
})
418+
});
419+
420+
(function it(_ = 'supports multiple handlers and unions their output types') {
421+
type SourceError = ParseError | ValidationError | Error
422+
type Expectation = Result<number | boolean, SourceError | Error>
423+
424+
const source: Result<number, SourceError> = err(new ValidationError('missing-field'))
425+
const result: Expectation = source.catchTags({
426+
ParseError: (_error) => ok(true),
427+
ValidationError: (_error) => err(new Error('validated')),
428+
})
429+
});
430+
});
431+
396432
(function describe(_ = 'match') {
397433
(function it(_ = 'the type of the arguments match the types of the result') {
398434
type OKExpectation = number
@@ -1693,6 +1729,41 @@ type CreateTuple<L, V = string> =
16931729
});
16941730
});
16951731

1732+
(function describe(_ = 'catchTags') {
1733+
class ParseError extends TaggedError<'ParseError'> {
1734+
constructor(readonly input: string) {
1735+
super('ParseError', input)
1736+
}
1737+
}
1738+
1739+
class ValidationError extends TaggedError<'ValidationError'> {
1740+
constructor(readonly reason: string) {
1741+
super('ValidationError', reason)
1742+
}
1743+
}
1744+
1745+
(function it(_ = 'suggests keys based on tagged errors in ResultAsync error union') {
1746+
type SourceError = ParseError | ValidationError | Error
1747+
type Expectation = ResultAsync<number | string, SourceError | Error>
1748+
1749+
const source: ResultAsync<number, SourceError> = errAsync(new ParseError('raw-input'))
1750+
const result: Expectation = source.catchTags({
1751+
ParseError: (_error) => okAsync('raw-input'),
1752+
})
1753+
});
1754+
1755+
(function it(_ = 'supports mixing Result and ResultAsync handlers') {
1756+
type SourceError = ParseError | ValidationError | Error
1757+
type Expectation = ResultAsync<number | boolean, SourceError | Error>
1758+
1759+
const source: ResultAsync<number, SourceError> = errAsync(new ValidationError('missing-field'))
1760+
const result: Expectation = source.catchTags({
1761+
ParseError: (_error) => ok(true),
1762+
ValidationError: (_error) => errAsync(new Error('validated')),
1763+
})
1764+
});
1765+
});
1766+
16961767
(function describe(_ = 'combine') {
16971768
(function it(_ = 'combines different result asyncs into one') {
16981769
type Expectation = ResultAsync<[ number, string, boolean, boolean ], Error | string | string[]>;

0 commit comments

Comments
 (0)