go-go-try 7.0.0 → 7.2.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 +182 -0
- package/README.md +261 -7
- package/dist/index.cjs +77 -29
- package/dist/index.d.cts +137 -21
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +137 -21
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +76 -29
- package/package.json +1 -1
- package/src/index.test.ts +581 -8
- package/src/index.ts +238 -55
package/AGENTS.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# go-go-try - Agent Guide
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
**go-go-try** is a TypeScript utility library for error handling inspired by Go's error handling pattern. It provides a functional approach to try/catch operations by returning a tuple `[error, value]` instead of throwing exceptions.
|
|
6
|
+
|
|
7
|
+
- **Name**: go-go-try
|
|
8
|
+
- **Version**: 6.2.0
|
|
9
|
+
- **License**: MIT
|
|
10
|
+
- **Repository**: thelinuxlich/go-go-try
|
|
11
|
+
- **Node.js Requirements**: >= 16
|
|
12
|
+
|
|
13
|
+
## Technology Stack
|
|
14
|
+
|
|
15
|
+
- **Language**: TypeScript 5.8.3
|
|
16
|
+
- **Build Tool**: [pkgroll](https://github.com/privatenumber/pkgroll) - A zero-config TypeScript package bundler
|
|
17
|
+
- **Linter**: [Biome](https://biomejs.dev/) - Fast linter and formatter
|
|
18
|
+
- **Test Framework**: [Vitest](https://vitest.dev/) - Vite-native unit test framework
|
|
19
|
+
- **Type Testing**: [@ark/attest](https://github.com/arktypeio/arktype) - Runtime type assertions for TypeScript
|
|
20
|
+
|
|
21
|
+
## Project Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
.
|
|
25
|
+
├── src/
|
|
26
|
+
│ ├── index.ts # Main source file - exports goTry, goTryRaw, and utility types
|
|
27
|
+
│ └── index.test.ts # Comprehensive test suite with runtime and type tests
|
|
28
|
+
├── dist/ # Build output (generated by pkgroll)
|
|
29
|
+
│ ├── index.cjs # CommonJS build
|
|
30
|
+
│ ├── index.mjs # ES Module build
|
|
31
|
+
│ ├── index.d.cts # CommonJS type definitions
|
|
32
|
+
│ └── index.d.mts # ES Module type definitions
|
|
33
|
+
├── .github/workflows/
|
|
34
|
+
│ └── main.yml # CI configuration for GitHub Actions
|
|
35
|
+
├── .attest/ # Ark attest cache directory
|
|
36
|
+
├── package.json # Package configuration with dual CJS/ESM exports
|
|
37
|
+
├── tsconfig.json # TypeScript strict configuration
|
|
38
|
+
├── vitest.config.ts # Vitest configuration with type checking enabled
|
|
39
|
+
├── setupVitest.ts # Vitest global setup for @ark/attest
|
|
40
|
+
└── README.md # User-facing documentation
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Build and Test Commands
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Build the project (generates dist/ with CJS, ESM, and type definitions)
|
|
47
|
+
npm run build
|
|
48
|
+
|
|
49
|
+
# Run linting with auto-fix
|
|
50
|
+
npm run lint
|
|
51
|
+
|
|
52
|
+
# Run the full test suite (build + lint + vitest)
|
|
53
|
+
npm test
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The test script is a composite that:
|
|
57
|
+
1. Builds the project
|
|
58
|
+
2. Runs the linter
|
|
59
|
+
3. Executes Vitest tests
|
|
60
|
+
|
|
61
|
+
## Code Style Guidelines
|
|
62
|
+
|
|
63
|
+
- **Linter**: Biome is used for linting and formatting
|
|
64
|
+
- **Configuration**: Uses Biome's default configuration (no biome.json present)
|
|
65
|
+
- **Strict TypeScript**: The `tsconfig.json` enforces strict mode with additional checks:
|
|
66
|
+
- `noUnusedLocals`: true
|
|
67
|
+
- `noUnusedParameters`: true
|
|
68
|
+
- `allowUnreachableCode`: false
|
|
69
|
+
- `noUncheckedIndexedAccess`: true
|
|
70
|
+
- `noFallthroughCasesInSwitch`: true
|
|
71
|
+
- `forceConsistentCasingInFileNames`: true
|
|
72
|
+
|
|
73
|
+
## Testing Instructions
|
|
74
|
+
|
|
75
|
+
### Test Structure
|
|
76
|
+
|
|
77
|
+
Tests are co-located with source code in `src/index.test.ts` using Vitest.
|
|
78
|
+
|
|
79
|
+
### Test Types
|
|
80
|
+
|
|
81
|
+
1. **Runtime Tests**: Standard unit tests verifying behavior
|
|
82
|
+
2. **Type Tests**: Using `@ark/attest` to verify TypeScript type inference at runtime
|
|
83
|
+
|
|
84
|
+
### Key Testing Patterns
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// Runtime test
|
|
88
|
+
import { assert, test } from 'vitest'
|
|
89
|
+
test('description', () => {
|
|
90
|
+
const result = goTry(() => 'value')
|
|
91
|
+
assert.equal(result[1], 'value')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Type test
|
|
95
|
+
import { attest } from '@ark/attest'
|
|
96
|
+
test('types are correct', () => {
|
|
97
|
+
const result = goTry('value')
|
|
98
|
+
attest<Result<string, string>>(result)
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Running Tests
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Run all tests with type checking
|
|
106
|
+
npx vitest run
|
|
107
|
+
|
|
108
|
+
# Run tests in watch mode (during development)
|
|
109
|
+
npx vitest
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Global Setup
|
|
113
|
+
|
|
114
|
+
The `setupVitest.ts` file configures `@ark/attest` for type assertions in tests.
|
|
115
|
+
|
|
116
|
+
## API Design
|
|
117
|
+
|
|
118
|
+
### Core Functions
|
|
119
|
+
|
|
120
|
+
- **`goTry<T>(value)`**: Returns `[string | undefined, T | undefined]` - error is the message string
|
|
121
|
+
- **`goTryRaw<T, E>(value)`**: Returns `[E | undefined, T | undefined]` - error is the raw Error object
|
|
122
|
+
|
|
123
|
+
### Type Helpers
|
|
124
|
+
|
|
125
|
+
- **`Result<E, T>`**: The tuple type `[E | undefined, T | undefined]`
|
|
126
|
+
- **`Success<T>`**: `[undefined, T]`
|
|
127
|
+
- **`Failure<E>`**: `[E, undefined]`
|
|
128
|
+
- **`isSuccess(result)`**: Type guard to check if result is success
|
|
129
|
+
- **`isFailure(result)`**: Type guard to check if result is failure
|
|
130
|
+
|
|
131
|
+
### Input Handling
|
|
132
|
+
|
|
133
|
+
Both functions accept:
|
|
134
|
+
- Direct values
|
|
135
|
+
- Functions (sync or async)
|
|
136
|
+
- Promises
|
|
137
|
+
|
|
138
|
+
## Dual Package Support
|
|
139
|
+
|
|
140
|
+
The package supports both CommonJS and ESM consumers:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"main": "./dist/index.cjs",
|
|
145
|
+
"module": "./dist/index.mjs",
|
|
146
|
+
"types": "./dist/index.d.mts",
|
|
147
|
+
"exports": {
|
|
148
|
+
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" },
|
|
149
|
+
"import": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## CI/CD
|
|
155
|
+
|
|
156
|
+
GitHub Actions workflow (`.github/workflows/main.yml`) runs on every push and PR:
|
|
157
|
+
|
|
158
|
+
- Tests against Node.js versions: 12, 14, 16, 18
|
|
159
|
+
- Uses `yarn install` and `yarn test`
|
|
160
|
+
- `fail-fast: false` to see results for all Node versions
|
|
161
|
+
|
|
162
|
+
## Security Considerations
|
|
163
|
+
|
|
164
|
+
- Zero runtime dependencies - reduces supply chain attack surface
|
|
165
|
+
- Dev dependencies are locked via `package-lock.json`
|
|
166
|
+
- Uses `type: "module"` for native ESM support
|
|
167
|
+
|
|
168
|
+
## Development Workflow
|
|
169
|
+
|
|
170
|
+
1. Make changes to `src/index.ts`
|
|
171
|
+
2. Add/update tests in `src/index.test.ts`
|
|
172
|
+
3. Run `npm test` to verify build, lint, and tests pass
|
|
173
|
+
4. The CI will test against multiple Node.js versions on push
|
|
174
|
+
|
|
175
|
+
## Notes for AI Agents
|
|
176
|
+
|
|
177
|
+
- Always run `npm test` after making changes to ensure build, lint, and tests pass
|
|
178
|
+
- Type tests with `@ark/attest` are as important as runtime tests
|
|
179
|
+
- The library has zero dependencies - avoid adding any
|
|
180
|
+
- Maintain dual CJS/ESM compatibility when making changes
|
|
181
|
+
- Follow the existing function overload patterns for type inference
|
|
182
|
+
- Error handling should preserve the Go-style tuple return pattern
|
package/README.md
CHANGED
|
@@ -99,8 +99,8 @@ const [errors, results] = await goTryAll([
|
|
|
99
99
|
fetchComments(userId)
|
|
100
100
|
])
|
|
101
101
|
|
|
102
|
-
// errors is
|
|
103
|
-
// results is
|
|
102
|
+
// errors is [string | undefined, string | undefined, string | undefined]
|
|
103
|
+
// results is [User | undefined, Posts | undefined, Comments | undefined]
|
|
104
104
|
|
|
105
105
|
const [user, posts, comments] = results
|
|
106
106
|
```
|
|
@@ -202,6 +202,88 @@ if (err === undefined) {
|
|
|
202
202
|
}
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
+
### Tagged Errors for Discriminated Unions
|
|
206
|
+
|
|
207
|
+
Create typed errors with a `_tag` property for pattern matching and discriminated unions:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { taggedError, goTryRaw, failure, type Result } from 'go-go-try'
|
|
211
|
+
|
|
212
|
+
// Define error types
|
|
213
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
214
|
+
const NetworkError = taggedError('NetworkError')
|
|
215
|
+
const ValidationError = taggedError('ValidationError')
|
|
216
|
+
|
|
217
|
+
// Create a union type
|
|
218
|
+
import type { TaggedUnion } from 'go-go-try'
|
|
219
|
+
|
|
220
|
+
// Option 1: Using TaggedUnion helper (cleaner)
|
|
221
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
222
|
+
const NetworkError = taggedError('NetworkError')
|
|
223
|
+
const ValidationError = taggedError('ValidationError')
|
|
224
|
+
|
|
225
|
+
type AppError = TaggedUnion<[typeof DatabaseError, typeof NetworkError, typeof ValidationError]>
|
|
226
|
+
// Equivalent to: DatabaseError | NetworkError | ValidationError
|
|
227
|
+
|
|
228
|
+
// Option 2: Using InstanceType (standard TypeScript)
|
|
229
|
+
type AppErrorVerbose =
|
|
230
|
+
| InstanceType<typeof DatabaseError>
|
|
231
|
+
| InstanceType<typeof NetworkError>
|
|
232
|
+
| InstanceType<typeof ValidationError>
|
|
233
|
+
|
|
234
|
+
// Use in functions with typed error returns
|
|
235
|
+
async function fetchUser(id: string): Promise<Result<AppError, User>> {
|
|
236
|
+
const [dbErr, user] = await goTryRaw(queryDatabase(id), DatabaseError)
|
|
237
|
+
if (dbErr) return failure(dbErr)
|
|
238
|
+
|
|
239
|
+
const [netErr, enriched] = await goTryRaw(enrichUserData(user!), NetworkError)
|
|
240
|
+
if (netErr) return failure(netErr)
|
|
241
|
+
|
|
242
|
+
return [undefined, enriched] as const
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Pattern matching on errors
|
|
246
|
+
const [err, user] = await fetchUser('123')
|
|
247
|
+
if (err) {
|
|
248
|
+
switch (err._tag) {
|
|
249
|
+
case 'DatabaseError':
|
|
250
|
+
console.error('Database failed:', err.message)
|
|
251
|
+
break
|
|
252
|
+
case 'NetworkError':
|
|
253
|
+
console.error('Network issue:', err.message)
|
|
254
|
+
break
|
|
255
|
+
case 'ValidationError':
|
|
256
|
+
console.error('Invalid data:', err.message)
|
|
257
|
+
break
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Exhaustive switch with compile-time safety
|
|
262
|
+
function assertNever(value: never): never {
|
|
263
|
+
throw new Error(`Unhandled case: ${String(value)}`)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function handleError(err: AppError): string {
|
|
267
|
+
switch (err._tag) {
|
|
268
|
+
case 'DatabaseError':
|
|
269
|
+
return `DB: ${err.message}`
|
|
270
|
+
case 'NetworkError':
|
|
271
|
+
return `NET: ${err.message}`
|
|
272
|
+
case 'ValidationError':
|
|
273
|
+
return `VAL: ${err.message}`
|
|
274
|
+
default:
|
|
275
|
+
// TypeScript will error here if any case is missing above
|
|
276
|
+
return assertNever(err)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The `taggedError` function creates an error class with:
|
|
282
|
+
- `_tag`: A readonly string literal for discriminated unions
|
|
283
|
+
- `message`: The error message
|
|
284
|
+
- `cause`: Optional cause for error chaining
|
|
285
|
+
- `name`: Set to the tag value
|
|
286
|
+
|
|
205
287
|
### Helper Functions
|
|
206
288
|
|
|
207
289
|
Build custom utilities on top of the primitives:
|
|
@@ -236,22 +318,86 @@ Executes a function, promise, or value and returns a Result type with error mess
|
|
|
236
318
|
function goTry<T>(value: T | Promise<T> | (() => T | Promise<T>)): Result<string, T> | Promise<Result<string, T>>
|
|
237
319
|
```
|
|
238
320
|
|
|
239
|
-
### `goTryRaw<T, E>(value)`
|
|
321
|
+
### `goTryRaw<T, E>(value, ErrorClass?)`
|
|
240
322
|
|
|
241
323
|
Like `goTry` but returns the raw Error object instead of just the message.
|
|
242
324
|
|
|
325
|
+
Optionally accepts an error constructor to wrap caught errors - useful with `taggedError` for discriminated unions.
|
|
326
|
+
|
|
243
327
|
```ts
|
|
328
|
+
// Without ErrorClass - err is Error | undefined
|
|
244
329
|
function goTryRaw<T, E = Error>(value: T | Promise<T> | (() => T | Promise<T>)): Result<E, T> | Promise<Result<E, T>>
|
|
330
|
+
|
|
331
|
+
// With ErrorClass - err is E | undefined (e.g., DatabaseError | undefined)
|
|
332
|
+
function goTryRaw<T, E>(value: T | Promise<T> | (() => T | Promise<T>), ErrorClass: ErrorConstructor<E>): Result<E, T> | Promise<Result<E, T>>
|
|
245
333
|
```
|
|
246
334
|
|
|
247
|
-
|
|
335
|
+
**Example:**
|
|
336
|
+
```ts
|
|
337
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
248
338
|
|
|
249
|
-
|
|
339
|
+
// Raw error (default)
|
|
340
|
+
const [err1, data1] = await goTryRaw(fetchData())
|
|
341
|
+
// err1 is Error | undefined
|
|
342
|
+
|
|
343
|
+
// Tagged error
|
|
344
|
+
const [err2, data2] = await goTryRaw(fetchData(), DatabaseError)
|
|
345
|
+
// err2 is DatabaseError | undefined
|
|
346
|
+
// err2._tag is 'DatabaseError' - enables discriminated unions
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### `goTryAll<T>(items, options?)`
|
|
350
|
+
|
|
351
|
+
Executes multiple promises or factory functions with optional concurrency limit. Returns a tuple of `[errors, results]` with fixed tuple types preserving input order.
|
|
250
352
|
|
|
251
353
|
```ts
|
|
354
|
+
interface GoTryAllOptions {
|
|
355
|
+
concurrency?: number // 0 = unlimited (default), 1 = sequential, N = max concurrent
|
|
356
|
+
}
|
|
357
|
+
|
|
252
358
|
function goTryAll<T extends readonly unknown[]>(
|
|
253
|
-
|
|
254
|
-
|
|
359
|
+
items: { [K in keyof T]: Promise<T[K]> | (() => Promise<T[K]>) },
|
|
360
|
+
options?: GoTryAllOptions
|
|
361
|
+
): Promise<[{ [K in keyof T]: string | undefined }, { [K in keyof T]: T[K] | undefined }]>
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Promise mode** (pass promises directly):
|
|
365
|
+
```ts
|
|
366
|
+
// Run all in parallel (default):
|
|
367
|
+
const [errors, results] = await goTryAll([
|
|
368
|
+
fetchUser(1), // Promise<User>
|
|
369
|
+
fetchUser(2), // Promise<User>
|
|
370
|
+
fetchUser(3), // Promise<User>
|
|
371
|
+
])
|
|
372
|
+
// errors: [string | undefined, string | undefined, string | undefined]
|
|
373
|
+
// results: [User | undefined, User | undefined, User | undefined]
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Factory mode** (pass functions that return promises):
|
|
377
|
+
```ts
|
|
378
|
+
// True lazy execution - factories only called when a slot is available
|
|
379
|
+
const [errors, results] = await goTryAll([
|
|
380
|
+
() => fetchUser(1), // Only called when concurrency slot available
|
|
381
|
+
() => fetchUser(2), // Only called when concurrency slot available
|
|
382
|
+
() => fetchUser(3), // Only called when concurrency slot available
|
|
383
|
+
() => fetchUser(4), // Only called when concurrency slot available
|
|
384
|
+
], { concurrency: 2 })
|
|
385
|
+
|
|
386
|
+
// Use factory mode when you need to:
|
|
387
|
+
// - Rate limit API calls (don't start HTTP requests until allowed)
|
|
388
|
+
// - Control database connection limits
|
|
389
|
+
// - Limit expensive computation resources
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### `goTryAllRaw<T>(items, options?)`
|
|
393
|
+
|
|
394
|
+
Like `goTryAll`, but returns raw Error objects instead of error messages.
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
function goTryAllRaw<T extends readonly unknown[]>(
|
|
398
|
+
items: { [K in keyof T]: Promise<T[K]> | (() => Promise<T[K]>) },
|
|
399
|
+
options?: GoTryAllOptions
|
|
400
|
+
): Promise<[{ [K in keyof T]: Error | undefined }, { [K in keyof T]: T[K] | undefined }]>
|
|
255
401
|
```
|
|
256
402
|
|
|
257
403
|
### `goTryOr<T>(value, defaultValue)`
|
|
@@ -281,12 +427,120 @@ function success<T>(value: T): Success<T>
|
|
|
281
427
|
function failure<E>(error: E): Failure<E>
|
|
282
428
|
```
|
|
283
429
|
|
|
430
|
+
### `taggedError<T>(tag)`
|
|
431
|
+
|
|
432
|
+
Creates a tagged error class for discriminated error handling. Returns a class constructor that extends `Error` and includes a readonly `_tag` property.
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
function taggedError<T extends string>(tag: T): TaggedErrorClass<T>
|
|
436
|
+
|
|
437
|
+
// Returned class interface:
|
|
438
|
+
class TaggedErrorClass<T> extends Error implements TaggedError<T> {
|
|
439
|
+
readonly _tag: T
|
|
440
|
+
readonly cause?: unknown
|
|
441
|
+
constructor(message: string, options?: { cause?: unknown })
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Example:**
|
|
446
|
+
```ts
|
|
447
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
448
|
+
const err = new DatabaseError('connection failed', { cause: originalError })
|
|
449
|
+
|
|
450
|
+
console.log(err._tag) // 'DatabaseError'
|
|
451
|
+
console.log(err.message) // 'connection failed'
|
|
452
|
+
console.log(err.name) // 'DatabaseError'
|
|
453
|
+
console.log(err.cause) // originalError
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### `TaggedInstance<T>`
|
|
457
|
+
|
|
458
|
+
Extracts the instance type from a tagged error class. Cleaner alternative to `InstanceType<typeof ErrorClass>`.
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
type TaggedInstance<T extends ErrorConstructor<unknown>> =
|
|
462
|
+
T extends ErrorConstructor<infer E> ? E : never
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Example:**
|
|
466
|
+
```ts
|
|
467
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
468
|
+
type DbError = TaggedInstance<typeof DatabaseError>
|
|
469
|
+
// Equivalent to: InstanceType<typeof DatabaseError>
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### `TaggedUnion<T>`
|
|
473
|
+
|
|
474
|
+
Creates a union type from multiple tagged error classes.
|
|
475
|
+
|
|
476
|
+
```ts
|
|
477
|
+
type TaggedUnion<T extends readonly ErrorConstructor<unknown>[]> =
|
|
478
|
+
{ [K in keyof T]: T[K] extends ErrorConstructor<infer E> ? E : never }[number]
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Example:**
|
|
482
|
+
```ts
|
|
483
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
484
|
+
const NetworkError = taggedError('NetworkError')
|
|
485
|
+
const ValidationError = taggedError('ValidationError')
|
|
486
|
+
|
|
487
|
+
// Before (verbose):
|
|
488
|
+
type AppErrorVerbose =
|
|
489
|
+
| InstanceType<typeof DatabaseError>
|
|
490
|
+
| InstanceType<typeof NetworkError>
|
|
491
|
+
| InstanceType<typeof ValidationError>
|
|
492
|
+
|
|
493
|
+
// After (clean):
|
|
494
|
+
type AppError = TaggedUnion<[typeof DatabaseError, typeof NetworkError, typeof ValidationError]>
|
|
495
|
+
// Results in: DatabaseError | NetworkError | ValidationError
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
#### Automatic Union Inference
|
|
499
|
+
|
|
500
|
+
When using `goTryRaw` with different error classes in the same function, TypeScript **automatically infers** the union type without needing explicit type annotations:
|
|
501
|
+
|
|
502
|
+
```ts
|
|
503
|
+
// No explicit return type needed!
|
|
504
|
+
async function fetchUserData(id: string) {
|
|
505
|
+
// First operation might fail with DatabaseError
|
|
506
|
+
const [dbErr, user] = await goTryRaw(queryDb(id), DatabaseError)
|
|
507
|
+
if (dbErr) return failure(dbErr) // returns Failure<DatabaseError>
|
|
508
|
+
|
|
509
|
+
// Second operation might fail with NetworkError
|
|
510
|
+
const [netErr, enriched] = await goTryRaw(enrichUser(user!), NetworkError)
|
|
511
|
+
if (netErr) return failure(netErr) // returns Failure<NetworkError>
|
|
512
|
+
|
|
513
|
+
return success(enriched) // returns Success<User>
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// TypeScript infers: Promise<Result<DatabaseError | NetworkError, User>>
|
|
517
|
+
// No TaggedUnion or explicit types needed!
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
The inferred union enables exhaustive pattern matching:
|
|
521
|
+
|
|
522
|
+
```ts
|
|
523
|
+
const [err, user] = await fetchUserData('123')
|
|
524
|
+
if (err) {
|
|
525
|
+
switch (err._tag) {
|
|
526
|
+
case 'DatabaseError': /* handle db error */ break
|
|
527
|
+
case 'NetworkError': /* handle network error */ break
|
|
528
|
+
default: assertNever(err) // compile-time safety
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
284
533
|
## Types
|
|
285
534
|
|
|
286
535
|
```ts
|
|
287
536
|
type Success<T> = readonly [undefined, T]
|
|
288
537
|
type Failure<E> = readonly [E, undefined]
|
|
289
538
|
type Result<E, T> = Success<T> | Failure<E>
|
|
539
|
+
|
|
540
|
+
// Error type helpers
|
|
541
|
+
type TaggedInstance<T> = T extends ErrorConstructor<infer E> ? E : never
|
|
542
|
+
type TaggedUnion<T extends readonly ErrorConstructor<unknown>[]> =
|
|
543
|
+
{ [K in keyof T]: T[K] extends ErrorConstructor<infer E> ? E : never }[number]
|
|
290
544
|
```
|
|
291
545
|
|
|
292
546
|
## License
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
function taggedError(tag) {
|
|
4
|
+
return class TaggedErrorClass extends Error {
|
|
5
|
+
constructor(message, options) {
|
|
6
|
+
super(message);
|
|
7
|
+
this._tag = tag;
|
|
8
|
+
this.name = tag;
|
|
9
|
+
this.cause = options?.cause;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
3
13
|
function isSuccess(result) {
|
|
4
14
|
return result[0] === void 0;
|
|
5
15
|
}
|
|
@@ -26,34 +36,65 @@ function goTryOr(value, defaultValue) {
|
|
|
26
36
|
return [getErrorMessage(err), resolveDefault(defaultValue)];
|
|
27
37
|
}
|
|
28
38
|
}
|
|
29
|
-
async function
|
|
30
|
-
|
|
39
|
+
async function runWithConcurrency(items, concurrency) {
|
|
40
|
+
if (items.length === 0) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const isFactoryMode = typeof items[0] === "function";
|
|
44
|
+
if (!isFactoryMode && concurrency <= 0) {
|
|
45
|
+
return Promise.allSettled(items);
|
|
46
|
+
}
|
|
47
|
+
const results = new Array(items.length);
|
|
48
|
+
let index = 0;
|
|
49
|
+
async function worker() {
|
|
50
|
+
while (index < items.length) {
|
|
51
|
+
const currentIndex = index++;
|
|
52
|
+
try {
|
|
53
|
+
const item = items[currentIndex];
|
|
54
|
+
const value = isFactoryMode ? await item() : await item;
|
|
55
|
+
results[currentIndex] = { status: "fulfilled", value };
|
|
56
|
+
} catch (reason) {
|
|
57
|
+
results[currentIndex] = { status: "rejected", reason };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const workerCount = concurrency <= 0 ? items.length : Math.min(concurrency, items.length);
|
|
62
|
+
const workers = [];
|
|
63
|
+
for (let i = 0; i < workerCount; i++) {
|
|
64
|
+
workers.push(worker());
|
|
65
|
+
}
|
|
66
|
+
await Promise.all(workers);
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
async function goTryAll(items, options) {
|
|
70
|
+
const settled = await runWithConcurrency(items, options?.concurrency ?? 0);
|
|
31
71
|
const errors = [];
|
|
32
72
|
const results = [];
|
|
33
|
-
for (
|
|
73
|
+
for (let i = 0; i < settled.length; i++) {
|
|
74
|
+
const item = settled[i];
|
|
34
75
|
if (item.status === "fulfilled") {
|
|
35
|
-
errors
|
|
36
|
-
results
|
|
76
|
+
errors[i] = void 0;
|
|
77
|
+
results[i] = item.value;
|
|
37
78
|
} else {
|
|
38
|
-
errors
|
|
39
|
-
results
|
|
79
|
+
errors[i] = getErrorMessage(item.reason);
|
|
80
|
+
results[i] = void 0;
|
|
40
81
|
}
|
|
41
82
|
}
|
|
42
83
|
return [errors, results];
|
|
43
84
|
}
|
|
44
|
-
async function
|
|
45
|
-
const settled = await
|
|
85
|
+
async function goTryAllRaw(items, options) {
|
|
86
|
+
const settled = await runWithConcurrency(items, options?.concurrency ?? 0);
|
|
46
87
|
const errors = [];
|
|
47
88
|
const results = [];
|
|
48
|
-
for (
|
|
89
|
+
for (let i = 0; i < settled.length; i++) {
|
|
90
|
+
const item = settled[i];
|
|
49
91
|
if (item.status === "fulfilled") {
|
|
50
|
-
errors
|
|
51
|
-
results
|
|
92
|
+
errors[i] = void 0;
|
|
93
|
+
results[i] = item.value;
|
|
52
94
|
} else {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
results.push(void 0);
|
|
95
|
+
const reason = item.reason;
|
|
96
|
+
errors[i] = isError(reason) ? reason : new Error(String(reason));
|
|
97
|
+
results[i] = void 0;
|
|
57
98
|
}
|
|
58
99
|
}
|
|
59
100
|
return [errors, results];
|
|
@@ -87,33 +128,40 @@ function goTry(value) {
|
|
|
87
128
|
return failure(getErrorMessage(err));
|
|
88
129
|
}
|
|
89
130
|
}
|
|
90
|
-
function goTryRaw(value) {
|
|
131
|
+
function goTryRaw(value, ErrorClass) {
|
|
132
|
+
const wrapError = (err) => {
|
|
133
|
+
if (ErrorClass) {
|
|
134
|
+
if (err === void 0) {
|
|
135
|
+
return new ErrorClass("undefined");
|
|
136
|
+
}
|
|
137
|
+
if (isError(err)) {
|
|
138
|
+
return new ErrorClass(err.message, { cause: err });
|
|
139
|
+
}
|
|
140
|
+
return new ErrorClass(String(err));
|
|
141
|
+
}
|
|
142
|
+
if (err === void 0) {
|
|
143
|
+
return new Error("undefined");
|
|
144
|
+
}
|
|
145
|
+
return isError(err) ? err : new Error(String(err));
|
|
146
|
+
};
|
|
91
147
|
try {
|
|
92
148
|
const result = typeof value === "function" ? value() : value;
|
|
93
149
|
if (isPromise(result)) {
|
|
94
|
-
return result.then((resolvedValue) => success(resolvedValue)).catch((err) =>
|
|
95
|
-
if (err === void 0) {
|
|
96
|
-
return failure(new Error("undefined"));
|
|
97
|
-
}
|
|
98
|
-
return failure(
|
|
99
|
-
isError(err) ? err : new Error(String(err))
|
|
100
|
-
);
|
|
101
|
-
});
|
|
150
|
+
return result.then((resolvedValue) => success(resolvedValue)).catch((err) => failure(wrapError(err)));
|
|
102
151
|
}
|
|
103
152
|
return success(result);
|
|
104
153
|
} catch (err) {
|
|
105
|
-
return failure(
|
|
106
|
-
isError(err) ? err : new Error(String(err))
|
|
107
|
-
);
|
|
154
|
+
return failure(wrapError(err));
|
|
108
155
|
}
|
|
109
156
|
}
|
|
110
157
|
|
|
111
158
|
exports.failure = failure;
|
|
112
159
|
exports.goTry = goTry;
|
|
113
160
|
exports.goTryAll = goTryAll;
|
|
161
|
+
exports.goTryAllRaw = goTryAllRaw;
|
|
114
162
|
exports.goTryOr = goTryOr;
|
|
115
163
|
exports.goTryRaw = goTryRaw;
|
|
116
|
-
exports.goTrySettled = goTrySettled;
|
|
117
164
|
exports.isFailure = isFailure;
|
|
118
165
|
exports.isSuccess = isSuccess;
|
|
119
166
|
exports.success = success;
|
|
167
|
+
exports.taggedError = taggedError;
|