go-go-try 7.2.0 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +81 -23
- package/README.md +0 -16
- package/dist/index.cjs +82 -59
- package/dist/index.d.cts +176 -115
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +176 -115
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +81 -60
- package/package.json +1 -1
- package/src/assert.ts +59 -0
- package/src/goTry.ts +45 -0
- package/src/goTryAll.ts +141 -0
- package/src/goTryOr.ts +55 -0
- package/src/goTryRaw.ts +89 -0
- package/src/index.test.ts +188 -21
- package/src/index.ts +23 -474
- package/src/internals.ts +45 -0
- package/src/result-helpers.ts +46 -0
- package/src/tagged-error.ts +38 -0
- package/src/types.ts +50 -0
package/src/goTry.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Result } from './types.js'
|
|
2
|
+
import { success, failure } from './result-helpers.js'
|
|
3
|
+
import { isPromise, getErrorMessage } from './internals.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Executes a function, promise, or value and returns a Result type.
|
|
7
|
+
* If an error occurs, it returns a Failure with the error message as a string.
|
|
8
|
+
*
|
|
9
|
+
* @template T The type of the successful result
|
|
10
|
+
* @param {T | Promise<T> | (() => T | Promise<T>)} value - The value, promise, or function to execute
|
|
11
|
+
* @returns {Result<string, T> | Promise<Result<string, T>>} A Result type or a Promise of a Result type
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // With a value
|
|
15
|
+
* const [err, result] = goTry(42);
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // With a function
|
|
19
|
+
* const [err, result] = goTry(() => JSON.parse('{"key": "value"}'));
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // With a promise
|
|
23
|
+
* const [err, result] = await goTry(fetch('https://api.example.com/data'));
|
|
24
|
+
*/
|
|
25
|
+
export function goTry<T>(fn: () => never): Result<string, never>
|
|
26
|
+
export function goTry<T>(fn: () => Promise<T>): Promise<Result<string, T>>
|
|
27
|
+
export function goTry<T>(promise: Promise<T>): Promise<Result<string, T>>
|
|
28
|
+
export function goTry<T>(fn: () => T): Result<string, T>
|
|
29
|
+
export function goTry<T>(value: T): Result<string, T>
|
|
30
|
+
export function goTry<T>(
|
|
31
|
+
value: T | Promise<T> | (() => T | Promise<T>),
|
|
32
|
+
): Result<string, T> | Promise<Result<string, T>> {
|
|
33
|
+
try {
|
|
34
|
+
const result =
|
|
35
|
+
typeof value === 'function' ? (value as () => T | Promise<T>)() : value
|
|
36
|
+
if (isPromise<T>(result)) {
|
|
37
|
+
return result
|
|
38
|
+
.then((resolvedValue) => success<T>(resolvedValue))
|
|
39
|
+
.catch((err) => failure<string>(getErrorMessage(err)))
|
|
40
|
+
}
|
|
41
|
+
return success<T>(result)
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return failure<string>(getErrorMessage(err))
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/goTryAll.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { GoTryAllOptions } from './types.js'
|
|
2
|
+
import { isError, getErrorMessage, type PromiseFactory } from './internals.js'
|
|
3
|
+
|
|
4
|
+
async function runWithConcurrency<T extends readonly unknown[]>(
|
|
5
|
+
items: { [K in keyof T]: Promise<T[K]> | PromiseFactory<T[K]> },
|
|
6
|
+
concurrency: number,
|
|
7
|
+
): Promise<PromiseSettledResult<T[number]>[]> {
|
|
8
|
+
if (items.length === 0) {
|
|
9
|
+
return []
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Auto-detect factory mode by checking if first item is a function
|
|
13
|
+
const isFactoryMode = typeof items[0] === 'function'
|
|
14
|
+
|
|
15
|
+
// concurrency of 0 means unlimited (run all in parallel)
|
|
16
|
+
if (!isFactoryMode && (concurrency <= 0)) {
|
|
17
|
+
return Promise.allSettled(items as Promise<T[number]>[])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const results: PromiseSettledResult<T[number]>[] = new Array(items.length)
|
|
21
|
+
let index = 0
|
|
22
|
+
|
|
23
|
+
async function worker(): Promise<void> {
|
|
24
|
+
while (index < items.length) {
|
|
25
|
+
const currentIndex = index++
|
|
26
|
+
try {
|
|
27
|
+
const item = items[currentIndex]
|
|
28
|
+
// If factory mode, call the function; otherwise await the promise directly
|
|
29
|
+
const value = isFactoryMode
|
|
30
|
+
? await (item as PromiseFactory<T[number]>)()
|
|
31
|
+
: await (item as Promise<T[number]>)
|
|
32
|
+
results[currentIndex] = { status: 'fulfilled', value }
|
|
33
|
+
} catch (reason) {
|
|
34
|
+
results[currentIndex] = { status: 'rejected', reason }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Determine number of workers
|
|
40
|
+
const workerCount = concurrency <= 0 ? items.length : Math.min(concurrency, items.length)
|
|
41
|
+
|
|
42
|
+
// Start workers
|
|
43
|
+
const workers: Promise<void>[] = []
|
|
44
|
+
for (let i = 0; i < workerCount; i++) {
|
|
45
|
+
workers.push(worker())
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await Promise.all(workers)
|
|
49
|
+
return results
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Executes multiple promises or factory functions in parallel (or with limited concurrency)
|
|
54
|
+
* and returns a tuple of [errors, results]. Unlike Promise.all, this doesn't fail fast -
|
|
55
|
+
* it waits for all promises to settle.
|
|
56
|
+
*
|
|
57
|
+
* Accepts either:
|
|
58
|
+
* - An array of promises (for simple parallel execution)
|
|
59
|
+
* - An array of factory functions that return promises (for lazy execution with concurrency control)
|
|
60
|
+
*
|
|
61
|
+
* @template T The tuple type of all promise results
|
|
62
|
+
* @param {readonly [...{ [K in keyof T]: Promise<T[K]> | (() => Promise<T[K]>) }]} items - Array of promises or factories
|
|
63
|
+
* @param {GoTryAllOptions} options - Optional configuration
|
|
64
|
+
* @returns {Promise<[{ [K in keyof T]: string | undefined }, { [K in keyof T]: T[K] | undefined }]>}
|
|
65
|
+
* A tuple where the first element is a tuple of errors (or undefined) and
|
|
66
|
+
* the second element is a tuple of results (or undefined), preserving input order
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // Run all in parallel (default) - with promises
|
|
70
|
+
* const [errors, results] = await goTryAll([
|
|
71
|
+
* fetchUser(1),
|
|
72
|
+
* fetchUser(2),
|
|
73
|
+
* fetchUser(3)
|
|
74
|
+
* ])
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* // Limit concurrency with factory functions (lazy execution)
|
|
78
|
+
* const [errors, results] = await goTryAll([
|
|
79
|
+
* () => fetchUser(1), // Only called when a slot is available
|
|
80
|
+
* () => fetchUser(2), // Only called when a slot is available
|
|
81
|
+
* () => fetchUser(3), // Only called when a slot is available
|
|
82
|
+
* ], { concurrency: 2 })
|
|
83
|
+
*/
|
|
84
|
+
export async function goTryAll<T extends readonly unknown[]>(
|
|
85
|
+
items: { [K in keyof T]: Promise<T[K]> | (() => Promise<T[K]>) },
|
|
86
|
+
options?: GoTryAllOptions,
|
|
87
|
+
): Promise<[{ [K in keyof T]: string | undefined }, { [K in keyof T]: T[K] | undefined }]> {
|
|
88
|
+
const settled = await runWithConcurrency(items, options?.concurrency ?? 0)
|
|
89
|
+
|
|
90
|
+
const errors = [] as { [K in keyof T]: string | undefined }
|
|
91
|
+
const results = [] as { [K in keyof T]: T[K] | undefined }
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < settled.length; i++) {
|
|
94
|
+
const item = settled[i]!
|
|
95
|
+
if (item.status === 'fulfilled') {
|
|
96
|
+
;(errors as (string | undefined)[])[i] = undefined
|
|
97
|
+
;(results as unknown[])[i] = (item as PromiseFulfilledResult<T[number]>).value
|
|
98
|
+
} else {
|
|
99
|
+
;(errors as (string | undefined)[])[i] = getErrorMessage((item as PromiseRejectedResult).reason)
|
|
100
|
+
;(results as unknown[])[i] = undefined
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return [errors, results]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Like `goTryAll`, but returns raw Error objects instead of error messages.
|
|
109
|
+
*
|
|
110
|
+
* @template T The tuple type of all promise results
|
|
111
|
+
* @param {readonly [...{ [K in keyof T]: Promise<T[K]> | (() => Promise<T[K]>) }]} items - Array of promises or factories
|
|
112
|
+
* @param {GoTryAllOptions} options - Optional configuration
|
|
113
|
+
* @returns {Promise<[{ [K in keyof T]: Error | undefined }, { [K in keyof T]: T[K] | undefined }]>}
|
|
114
|
+
* A tuple where the first element is a tuple of Error objects (or undefined) and
|
|
115
|
+
* the second element is a tuple of results (or undefined), preserving input order
|
|
116
|
+
*/
|
|
117
|
+
export async function goTryAllRaw<T extends readonly unknown[]>(
|
|
118
|
+
items: { [K in keyof T]: Promise<T[K]> | (() => Promise<T[K]>) },
|
|
119
|
+
options?: GoTryAllOptions,
|
|
120
|
+
): Promise<[{ [K in keyof T]: Error | undefined }, { [K in keyof T]: T[K] | undefined }]> {
|
|
121
|
+
const settled = await runWithConcurrency(items, options?.concurrency ?? 0)
|
|
122
|
+
|
|
123
|
+
const errors = [] as { [K in keyof T]: Error | undefined }
|
|
124
|
+
const results = [] as { [K in keyof T]: T[K] | undefined }
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < settled.length; i++) {
|
|
127
|
+
const item = settled[i]!
|
|
128
|
+
if (item.status === 'fulfilled') {
|
|
129
|
+
;(errors as (Error | undefined)[])[i] = undefined
|
|
130
|
+
;(results as unknown[])[i] = (item as PromiseFulfilledResult<T[number]>).value
|
|
131
|
+
} else {
|
|
132
|
+
const reason = (item as PromiseRejectedResult).reason
|
|
133
|
+
;(errors as (Error | undefined)[])[i] = isError(reason)
|
|
134
|
+
? reason
|
|
135
|
+
: new Error(String(reason))
|
|
136
|
+
;(results as unknown[])[i] = undefined
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return [errors, results]
|
|
141
|
+
}
|
package/src/goTryOr.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ResultWithDefault } from './types.js'
|
|
2
|
+
import { success } from './result-helpers.js'
|
|
3
|
+
import { isPromise, getErrorMessage, resolveDefault } from './internals.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Executes a function, promise, or value and returns a Result type with a fallback default.
|
|
7
|
+
* If an error occurs, it returns the error message and the default value.
|
|
8
|
+
*
|
|
9
|
+
* @template T The type of the successful result
|
|
10
|
+
* @param {T | Promise<T> | (() => T | Promise<T>)} value - The value, promise, or function to execute
|
|
11
|
+
* @param {T | (() => T)} defaultValue - The default value or a function to compute it (only called on failure)
|
|
12
|
+
* @returns {ResultWithDefault<string, T> | Promise<ResultWithDefault<string, T>>} A tuple of [error, value] or Promise thereof
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // With a static default
|
|
16
|
+
* const [err, config] = goTryOr(() => JSON.parse('invalid'), { port: 3000 })
|
|
17
|
+
* // err is the error message, config is { port: 3000 }
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // With a computed default (lazy evaluation)
|
|
21
|
+
* const [err, user] = await goTryOr(fetchUser(id), () => ({
|
|
22
|
+
* id: 'anonymous',
|
|
23
|
+
* name: 'Guest'
|
|
24
|
+
* }))
|
|
25
|
+
*/
|
|
26
|
+
export function goTryOr<T>(fn: () => never, defaultValue: T | (() => T)): ResultWithDefault<string, T>
|
|
27
|
+
export function goTryOr<T>(
|
|
28
|
+
fn: () => Promise<T>,
|
|
29
|
+
defaultValue: T | (() => T),
|
|
30
|
+
): Promise<ResultWithDefault<string, T>>
|
|
31
|
+
export function goTryOr<T>(
|
|
32
|
+
promise: Promise<T>,
|
|
33
|
+
defaultValue: T | (() => T),
|
|
34
|
+
): Promise<ResultWithDefault<string, T>>
|
|
35
|
+
export function goTryOr<T>(fn: () => T, defaultValue: T | (() => T)): ResultWithDefault<string, T>
|
|
36
|
+
export function goTryOr<T>(value: T, defaultValue: T | (() => T)): ResultWithDefault<string, T>
|
|
37
|
+
export function goTryOr<T>(
|
|
38
|
+
value: T | Promise<T> | (() => T | Promise<T>),
|
|
39
|
+
defaultValue: T | (() => T),
|
|
40
|
+
): ResultWithDefault<string, T> | Promise<ResultWithDefault<string, T>> {
|
|
41
|
+
try {
|
|
42
|
+
const result =
|
|
43
|
+
typeof value === 'function' ? (value as () => T | Promise<T>)() : value
|
|
44
|
+
|
|
45
|
+
if (isPromise<T>(result)) {
|
|
46
|
+
return result
|
|
47
|
+
.then((resolvedValue) => success<T>(resolvedValue))
|
|
48
|
+
.catch((err) => [getErrorMessage(err), resolveDefault(defaultValue)] as const)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return success<T>(result)
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return [getErrorMessage(err), resolveDefault(defaultValue)] as const
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/goTryRaw.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Result, ErrorConstructor } from './types.js'
|
|
2
|
+
import { success, failure } from './result-helpers.js'
|
|
3
|
+
import { isPromise, isError } from './internals.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Executes a function, promise, or value and returns a Result type.
|
|
7
|
+
* If an error occurs, it returns a Failure with the raw error object.
|
|
8
|
+
*
|
|
9
|
+
* @template T The type of the successful result
|
|
10
|
+
* @template E The type of the error, defaults to Error
|
|
11
|
+
* @param {T | Promise<T> | (() => T | Promise<T>)} value - The value, promise, or function to execute
|
|
12
|
+
* @param {ErrorConstructor<E>} [ErrorClass] - Optional error constructor to wrap caught errors
|
|
13
|
+
* @returns {Result<E, T> | Promise<Result<E, T>>} A Result type or a Promise of a Result type
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // With a value
|
|
17
|
+
* const [err, result] = goTryRaw(42);
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // With a function
|
|
21
|
+
* const [err, result] = goTryRaw(() => JSON.parse('{"key": "value"}'));
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // With a promise
|
|
25
|
+
* const [err, result] = await goTryRaw(fetch('https://api.example.com/data'));
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // With tagged error for discriminated unions
|
|
29
|
+
* const DatabaseError = taggedError('DatabaseError');
|
|
30
|
+
* const [err, result] = await goTryRaw(fetchData(), DatabaseError);
|
|
31
|
+
* // err is InstanceType<typeof DatabaseError> | undefined
|
|
32
|
+
*/
|
|
33
|
+
export function goTryRaw<T, E = Error>(fn: () => never): Result<E, never>
|
|
34
|
+
export function goTryRaw<T, E = Error>(fn: () => never, ErrorClass: ErrorConstructor<E>): Result<E, never>
|
|
35
|
+
export function goTryRaw<T, E = Error>(
|
|
36
|
+
fn: () => Promise<T>,
|
|
37
|
+
): Promise<Result<E, T>>
|
|
38
|
+
export function goTryRaw<T, E = Error>(
|
|
39
|
+
fn: () => Promise<T>,
|
|
40
|
+
ErrorClass: ErrorConstructor<E>,
|
|
41
|
+
): Promise<Result<E, T>>
|
|
42
|
+
export function goTryRaw<T, E = Error>(
|
|
43
|
+
promise: Promise<T>,
|
|
44
|
+
): Promise<Result<E, T>>
|
|
45
|
+
export function goTryRaw<T, E = Error>(
|
|
46
|
+
promise: Promise<T>,
|
|
47
|
+
ErrorClass: ErrorConstructor<E>,
|
|
48
|
+
): Promise<Result<E, T>>
|
|
49
|
+
export function goTryRaw<T, E = Error>(fn: () => T): Result<E, T>
|
|
50
|
+
export function goTryRaw<T, E = Error>(fn: () => T, ErrorClass: ErrorConstructor<E>): Result<E, T>
|
|
51
|
+
export function goTryRaw<T, E = Error>(value: T): Result<E, T>
|
|
52
|
+
export function goTryRaw<T, E = Error>(value: T, ErrorClass: ErrorConstructor<E>): Result<E, T>
|
|
53
|
+
export function goTryRaw<T, E = Error>(
|
|
54
|
+
value: T | Promise<T> | (() => T | Promise<T>),
|
|
55
|
+
ErrorClass?: ErrorConstructor<E>,
|
|
56
|
+
): Result<E, T> | Promise<Result<E, T>> {
|
|
57
|
+
// Helper to wrap error in the provided class or return as-is
|
|
58
|
+
const wrapError = (err: unknown): E => {
|
|
59
|
+
if (ErrorClass) {
|
|
60
|
+
if (err === undefined) {
|
|
61
|
+
return new ErrorClass('undefined')
|
|
62
|
+
}
|
|
63
|
+
if (isError(err)) {
|
|
64
|
+
return new ErrorClass(err.message, { cause: err })
|
|
65
|
+
}
|
|
66
|
+
return new ErrorClass(String(err))
|
|
67
|
+
}
|
|
68
|
+
// Default behavior: return raw error
|
|
69
|
+
if (err === undefined) {
|
|
70
|
+
return new Error('undefined') as unknown as E
|
|
71
|
+
}
|
|
72
|
+
return (
|
|
73
|
+
isError(err) ? err : new Error(String(err))
|
|
74
|
+
) as unknown as E
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const result =
|
|
79
|
+
typeof value === 'function' ? (value as () => T | Promise<T>)() : value
|
|
80
|
+
if (isPromise<T>(result)) {
|
|
81
|
+
return result
|
|
82
|
+
.then((resolvedValue) => success<T>(resolvedValue))
|
|
83
|
+
.catch((err) => failure<E>(wrapError(err)))
|
|
84
|
+
}
|
|
85
|
+
return success<T>(result)
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return failure<E>(wrapError(err))
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/index.test.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import { attest } from '@ark/attest'
|
|
2
2
|
import { assert, describe, test } from 'vitest'
|
|
3
|
-
|
|
4
|
-
// Helper for exhaustive switch checks - if this function is called,
|
|
5
|
-
// it means we forgot to handle a case in a switch statement
|
|
6
|
-
function assertNever(value: never): never {
|
|
7
|
-
throw new Error(`Unhandled case: ${String(value)}`)
|
|
8
|
-
}
|
|
9
3
|
import {
|
|
10
4
|
type Result,
|
|
11
|
-
type TaggedInstance,
|
|
12
5
|
type TaggedUnion,
|
|
6
|
+
assert as assertTry,
|
|
7
|
+
assertNever,
|
|
13
8
|
failure,
|
|
14
9
|
goTry,
|
|
15
10
|
goTryAll,
|
|
@@ -363,6 +358,190 @@ describe('edge cases', () => {
|
|
|
363
358
|
})
|
|
364
359
|
})
|
|
365
360
|
|
|
361
|
+
describe('assert helper', () => {
|
|
362
|
+
test('does not throw when condition is true', () => {
|
|
363
|
+
// Should not throw
|
|
364
|
+
assertTry(true, 'should not throw')
|
|
365
|
+
assertTry(1 > 0, new Error('should not throw'))
|
|
366
|
+
assertTry('truthy', 'should not throw')
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
test('throws with string message when condition is false', () => {
|
|
370
|
+
try {
|
|
371
|
+
assertTry(false, 'custom error message')
|
|
372
|
+
// Should not reach here
|
|
373
|
+
assert.equal(true, false)
|
|
374
|
+
} catch (err) {
|
|
375
|
+
assert.ok(err instanceof Error)
|
|
376
|
+
assert.equal((err as Error).message, 'custom error message')
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
test('throws with Error instance when condition is false', () => {
|
|
381
|
+
const customError = new Error('custom error instance')
|
|
382
|
+
try {
|
|
383
|
+
assertTry(false, customError)
|
|
384
|
+
// Should not reach here
|
|
385
|
+
assert.equal(true, false)
|
|
386
|
+
} catch (err) {
|
|
387
|
+
assert.equal(err, customError)
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
test('throws with tagged error when condition is false', () => {
|
|
392
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
393
|
+
try {
|
|
394
|
+
assertTry(false, new DatabaseError('database connection failed'))
|
|
395
|
+
// Should not reach here
|
|
396
|
+
assert.equal(true, false)
|
|
397
|
+
} catch (err) {
|
|
398
|
+
assert.ok(err instanceof DatabaseError)
|
|
399
|
+
assert.equal((err as InstanceType<typeof DatabaseError>)._tag, 'DatabaseError')
|
|
400
|
+
assert.equal((err as Error).message, 'database connection failed')
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
test('type narrowing works with Result types using err === undefined', () => {
|
|
405
|
+
const [err, value] = goTry(() => 'success')
|
|
406
|
+
|
|
407
|
+
// Before assert: err is string | undefined, value is string | undefined
|
|
408
|
+
attest<string | undefined>(err)
|
|
409
|
+
attest<string | undefined>(value)
|
|
410
|
+
|
|
411
|
+
// Using err === undefined provides the best type narrowing
|
|
412
|
+
assertTry(err === undefined, 'should have no error')
|
|
413
|
+
|
|
414
|
+
// TypeScript now knows err is undefined and value is string
|
|
415
|
+
attest<undefined>(err)
|
|
416
|
+
attest<string>(value)
|
|
417
|
+
assert.equal(value, 'success')
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
test('type narrowing works with err === undefined check', () => {
|
|
421
|
+
const [err, value] = goTryRaw(() => ({ id: 1, name: 'test' }))
|
|
422
|
+
|
|
423
|
+
// Before assert
|
|
424
|
+
attest<Error | undefined>(err)
|
|
425
|
+
attest<{ id: number; name: string } | undefined>(value)
|
|
426
|
+
|
|
427
|
+
// Using err === undefined pattern
|
|
428
|
+
assertTry(err === undefined, new Error('should have no error'))
|
|
429
|
+
|
|
430
|
+
// After assert: TypeScript knows err is undefined, value is defined
|
|
431
|
+
attest<{ id: number; name: string }>(value)
|
|
432
|
+
assert.deepEqual(value, { id: 1, name: 'test' })
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
test('type narrowing works with tagged errors', () => {
|
|
436
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
437
|
+
const [err, user] = goTryRaw(() => ({ id: '123', name: 'John' }), DatabaseError)
|
|
438
|
+
|
|
439
|
+
// Before assert
|
|
440
|
+
attest<InstanceType<typeof DatabaseError> | undefined>(err)
|
|
441
|
+
attest<{ id: string; name: string } | undefined>(user)
|
|
442
|
+
|
|
443
|
+
// Use assert with tagged error
|
|
444
|
+
assertTry(err === undefined, new DatabaseError('Failed to fetch user'))
|
|
445
|
+
|
|
446
|
+
// After assert: TypeScript knows err is undefined, user is defined
|
|
447
|
+
attest<{ id: string; name: string }>(user)
|
|
448
|
+
assert.deepEqual(user, { id: '123', name: 'John' })
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test('reduces boilerplate compared to if(err) throw', () => {
|
|
452
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
453
|
+
|
|
454
|
+
function fetchUserOldStyle(): Result<InstanceType<typeof DatabaseError>, { id: string }> {
|
|
455
|
+
const [err, user] = goTryRaw(() => ({ id: '123' }), DatabaseError)
|
|
456
|
+
if (err) return failure(err) // Old style
|
|
457
|
+
return [undefined, user] as const
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function fetchUserNewStyle(): Result<InstanceType<typeof DatabaseError>, { id: string }> {
|
|
461
|
+
const [err, user] = goTryRaw(() => ({ id: '123' }), DatabaseError)
|
|
462
|
+
assertTry(err === undefined, new DatabaseError('Failed to fetch user'))
|
|
463
|
+
// TypeScript now knows user is defined
|
|
464
|
+
return [undefined, user] as const
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const [err1, user1] = fetchUserOldStyle()
|
|
468
|
+
const [err2, user2] = fetchUserNewStyle()
|
|
469
|
+
|
|
470
|
+
assert.equal(err1, undefined)
|
|
471
|
+
assert.equal(err2, undefined)
|
|
472
|
+
assert.deepEqual(user1, { id: '123' })
|
|
473
|
+
assert.deepEqual(user2, { id: '123' })
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
test('works with falsy values as condition', () => {
|
|
477
|
+
// 0 is falsy - should throw
|
|
478
|
+
assert.throws(() => assertTry(0, 'zero is falsy'))
|
|
479
|
+
|
|
480
|
+
// empty string is falsy - should throw
|
|
481
|
+
assert.throws(() => assertTry('', 'empty string is falsy'))
|
|
482
|
+
|
|
483
|
+
// null is falsy - should throw
|
|
484
|
+
assert.throws(() => assertTry(null, 'null is falsy'))
|
|
485
|
+
|
|
486
|
+
// undefined is falsy - should throw
|
|
487
|
+
assert.throws(() => assertTry(undefined, 'undefined is falsy'))
|
|
488
|
+
|
|
489
|
+
// NaN is falsy - should throw
|
|
490
|
+
assert.throws(() => assertTry(Number.NaN, 'NaN is falsy'))
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
test('works with truthy values as condition', () => {
|
|
494
|
+
// non-zero number is truthy
|
|
495
|
+
assertTry(1, new Error('one is truthy'))
|
|
496
|
+
|
|
497
|
+
// non-empty string is truthy
|
|
498
|
+
assertTry('hello', new Error('string is truthy'))
|
|
499
|
+
|
|
500
|
+
// object is truthy
|
|
501
|
+
assertTry({}, new Error('object is truthy'))
|
|
502
|
+
|
|
503
|
+
// array is truthy
|
|
504
|
+
assertTry([], new Error('array is truthy'))
|
|
505
|
+
|
|
506
|
+
// true is truthy
|
|
507
|
+
assertTry(true, new Error('true is truthy'))
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
test('works with ErrorClass and message (shorter syntax)', () => {
|
|
511
|
+
const ValidationError = taggedError('ValidationError')
|
|
512
|
+
|
|
513
|
+
// Should not throw when condition is true
|
|
514
|
+
assertTry(5 > 0, ValidationError, 'Value must be positive')
|
|
515
|
+
|
|
516
|
+
// Should throw with instantiated error when condition is false
|
|
517
|
+
try {
|
|
518
|
+
assertTry(-1 > 0, ValidationError, 'Value must be positive')
|
|
519
|
+
assert.equal(true, false) // Should not reach here
|
|
520
|
+
} catch (err) {
|
|
521
|
+
assert.ok(err instanceof ValidationError)
|
|
522
|
+
assert.equal((err as InstanceType<typeof ValidationError>)._tag, 'ValidationError')
|
|
523
|
+
assert.equal((err as Error).message, 'Value must be positive')
|
|
524
|
+
}
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
test('shorter syntax with tagged errors provides type narrowing', () => {
|
|
528
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
529
|
+
const [err, user] = goTryRaw(() => ({ id: '123', name: 'John' }), DatabaseError)
|
|
530
|
+
|
|
531
|
+
// Before assert
|
|
532
|
+
attest<InstanceType<typeof DatabaseError> | undefined>(err)
|
|
533
|
+
attest<{ id: string; name: string } | undefined>(user)
|
|
534
|
+
|
|
535
|
+
// Use assert with shorter syntax
|
|
536
|
+
assertTry(err === undefined, DatabaseError, 'Failed to fetch user')
|
|
537
|
+
|
|
538
|
+
// After assert: TypeScript knows err is undefined, user is defined
|
|
539
|
+
attest<undefined>(err)
|
|
540
|
+
attest<{ id: string; name: string }>(user)
|
|
541
|
+
assert.deepEqual(user, { id: '123', name: 'John' })
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
|
|
366
545
|
describe('goTry type tests', () => {
|
|
367
546
|
test('synchronous function returns correct types', () => {
|
|
368
547
|
const fn = () => 'value'
|
|
@@ -1080,7 +1259,7 @@ describe('taggedError', () => {
|
|
|
1080
1259
|
case 'ValidationError':
|
|
1081
1260
|
return `VAL: ${err.message}`
|
|
1082
1261
|
default:
|
|
1083
|
-
return
|
|
1262
|
+
return assertNever(err)
|
|
1084
1263
|
}
|
|
1085
1264
|
}
|
|
1086
1265
|
|
|
@@ -1149,18 +1328,6 @@ describe('taggedError type tests', () => {
|
|
|
1149
1328
|
})
|
|
1150
1329
|
|
|
1151
1330
|
|
|
1152
|
-
describe('TaggedInstance type helper', () => {
|
|
1153
|
-
test('extracts instance type from tagged error class', () => {
|
|
1154
|
-
const DatabaseError = taggedError('DatabaseError')
|
|
1155
|
-
type DbError = TaggedInstance<typeof DatabaseError>
|
|
1156
|
-
|
|
1157
|
-
// DbError should be the instance type
|
|
1158
|
-
const err: DbError = new DatabaseError('fail')
|
|
1159
|
-
assert.equal(err._tag, 'DatabaseError')
|
|
1160
|
-
assert.equal(err.message, 'fail')
|
|
1161
|
-
})
|
|
1162
|
-
})
|
|
1163
|
-
|
|
1164
1331
|
describe('TaggedUnion type helper', () => {
|
|
1165
1332
|
test('creates union type from multiple error classes', () => {
|
|
1166
1333
|
const DatabaseError = taggedError('DatabaseError')
|
|
@@ -1189,7 +1356,7 @@ describe('TaggedUnion type helper', () => {
|
|
|
1189
1356
|
case 'ValidationError':
|
|
1190
1357
|
return `VAL: ${err.message}`
|
|
1191
1358
|
default:
|
|
1192
|
-
return
|
|
1359
|
+
return assertNever(err)
|
|
1193
1360
|
}
|
|
1194
1361
|
}
|
|
1195
1362
|
|