go-go-try 7.1.0 → 7.2.1
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 +195 -1
- package/dist/index.cjs +29 -12
- package/dist/index.d.cts +81 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +81 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +29 -13
- package/package.json +1 -1
- package/src/index.test.ts +406 -0
- package/src/index.ts +109 -15
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
type Success<T> = readonly [undefined, T];
|
|
2
2
|
type Failure<E> = readonly [E, undefined];
|
|
3
3
|
type Result<E, T> = Success<T> | Failure<E>;
|
|
4
|
+
/**
|
|
5
|
+
* Base interface for tagged errors.
|
|
6
|
+
* The `_tag` property enables discriminated union narrowing.
|
|
7
|
+
*/
|
|
8
|
+
interface TaggedError<T extends string> {
|
|
9
|
+
readonly _tag: T;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly cause?: unknown;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates a tagged error class for discriminated error handling.
|
|
15
|
+
*
|
|
16
|
+
* @template T The literal type of the tag
|
|
17
|
+
* @param tag The string tag to identify this error type (e.g., 'DatabaseError')
|
|
18
|
+
* @returns A class constructor for creating tagged errors
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const DatabaseError = taggedError('DatabaseError')
|
|
22
|
+
* const NetworkError = taggedError('NetworkError')
|
|
23
|
+
*
|
|
24
|
+
* type MyError = InstanceType<typeof DatabaseError> | InstanceType<typeof NetworkError>
|
|
25
|
+
*
|
|
26
|
+
* function fetchUser(id: string): Result<MyError, User> {
|
|
27
|
+
* const [err, user] = goTryRaw(fetch(`/users/${id}`), DatabaseError)
|
|
28
|
+
* if (err) return failure(err)
|
|
29
|
+
* // ...
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* // Pattern matching on errors
|
|
33
|
+
* if (err._tag === 'DatabaseError') {
|
|
34
|
+
* // TypeScript knows this is DatabaseError
|
|
35
|
+
* }
|
|
36
|
+
*/
|
|
37
|
+
declare function taggedError<T extends string>(tag: T): {
|
|
38
|
+
new (message: string, options?: {
|
|
39
|
+
cause?: unknown;
|
|
40
|
+
}): {
|
|
41
|
+
readonly _tag: T;
|
|
42
|
+
readonly cause?: unknown;
|
|
43
|
+
name: string;
|
|
44
|
+
message: string;
|
|
45
|
+
stack?: string;
|
|
46
|
+
};
|
|
47
|
+
isError(error: unknown): error is Error;
|
|
48
|
+
};
|
|
4
49
|
type ResultWithDefault<E, T> = readonly [E | undefined, T];
|
|
5
50
|
type MaybePromise<T> = T | Promise<T>;
|
|
6
51
|
interface GoTryAllOptions {
|
|
@@ -121,6 +166,28 @@ declare function goTry<T>(fn: () => Promise<T>): Promise<Result<string, T>>;
|
|
|
121
166
|
declare function goTry<T>(promise: Promise<T>): Promise<Result<string, T>>;
|
|
122
167
|
declare function goTry<T>(fn: () => T): Result<string, T>;
|
|
123
168
|
declare function goTry<T>(value: T): Result<string, T>;
|
|
169
|
+
/**
|
|
170
|
+
* Type for error constructors that can be used with goTryRaw.
|
|
171
|
+
*/
|
|
172
|
+
type ErrorConstructor<E> = new (message: string, options?: {
|
|
173
|
+
cause?: unknown;
|
|
174
|
+
}) => E;
|
|
175
|
+
/**
|
|
176
|
+
* Creates a union type from multiple tagged error classes.
|
|
177
|
+
*
|
|
178
|
+
* @template T A tuple of tagged error class types
|
|
179
|
+
* @returns A union of all instance types
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* const DatabaseError = taggedError('DatabaseError')
|
|
183
|
+
* const NetworkError = taggedError('NetworkError')
|
|
184
|
+
*
|
|
185
|
+
* type AppError = TaggedUnion<[typeof DatabaseError, typeof NetworkError]>
|
|
186
|
+
* // Equivalent to: DatabaseError | NetworkError
|
|
187
|
+
*/
|
|
188
|
+
type TaggedUnion<T extends readonly ErrorConstructor<unknown>[]> = {
|
|
189
|
+
[K in keyof T]: T[K] extends ErrorConstructor<infer E> ? E : never;
|
|
190
|
+
}[number];
|
|
124
191
|
/**
|
|
125
192
|
* Executes a function, promise, or value and returns a Result type.
|
|
126
193
|
* If an error occurs, it returns a Failure with the raw error object.
|
|
@@ -128,6 +195,7 @@ declare function goTry<T>(value: T): Result<string, T>;
|
|
|
128
195
|
* @template T The type of the successful result
|
|
129
196
|
* @template E The type of the error, defaults to Error
|
|
130
197
|
* @param {T | Promise<T> | (() => T | Promise<T>)} value - The value, promise, or function to execute
|
|
198
|
+
* @param {ErrorConstructor<E>} [ErrorClass] - Optional error constructor to wrap caught errors
|
|
131
199
|
* @returns {Result<E, T> | Promise<Result<E, T>>} A Result type or a Promise of a Result type
|
|
132
200
|
*
|
|
133
201
|
* @example
|
|
@@ -141,13 +209,24 @@ declare function goTry<T>(value: T): Result<string, T>;
|
|
|
141
209
|
* @example
|
|
142
210
|
* // With a promise
|
|
143
211
|
* const [err, result] = await goTryRaw(fetch('https://api.example.com/data'));
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* // With tagged error for discriminated unions
|
|
215
|
+
* const DatabaseError = taggedError('DatabaseError');
|
|
216
|
+
* const [err, result] = await goTryRaw(fetchData(), DatabaseError);
|
|
217
|
+
* // err is InstanceType<typeof DatabaseError> | undefined
|
|
144
218
|
*/
|
|
145
219
|
declare function goTryRaw<T, E = Error>(fn: () => never): Result<E, never>;
|
|
220
|
+
declare function goTryRaw<T, E = Error>(fn: () => never, ErrorClass: ErrorConstructor<E>): Result<E, never>;
|
|
146
221
|
declare function goTryRaw<T, E = Error>(fn: () => Promise<T>): Promise<Result<E, T>>;
|
|
222
|
+
declare function goTryRaw<T, E = Error>(fn: () => Promise<T>, ErrorClass: ErrorConstructor<E>): Promise<Result<E, T>>;
|
|
147
223
|
declare function goTryRaw<T, E = Error>(promise: Promise<T>): Promise<Result<E, T>>;
|
|
224
|
+
declare function goTryRaw<T, E = Error>(promise: Promise<T>, ErrorClass: ErrorConstructor<E>): Promise<Result<E, T>>;
|
|
148
225
|
declare function goTryRaw<T, E = Error>(fn: () => T): Result<E, T>;
|
|
226
|
+
declare function goTryRaw<T, E = Error>(fn: () => T, ErrorClass: ErrorConstructor<E>): Result<E, T>;
|
|
149
227
|
declare function goTryRaw<T, E = Error>(value: T): Result<E, T>;
|
|
228
|
+
declare function goTryRaw<T, E = Error>(value: T, ErrorClass: ErrorConstructor<E>): Result<E, T>;
|
|
150
229
|
|
|
151
|
-
export { failure, goTry, goTryAll, goTryAllRaw, goTryOr, goTryRaw, isFailure, isSuccess, success };
|
|
152
|
-
export type { Failure, GoTryAllOptions, MaybePromise, Result, ResultWithDefault, Success };
|
|
230
|
+
export { failure, goTry, goTryAll, goTryAllRaw, goTryOr, goTryRaw, isFailure, isSuccess, success, taggedError };
|
|
231
|
+
export type { ErrorConstructor, Failure, GoTryAllOptions, MaybePromise, Result, ResultWithDefault, Success, TaggedError, TaggedUnion };
|
|
153
232
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sources":["../src/index.ts"],"mappings":"KAAY,OAAO,CAAC,CAAC,IAAI,SAAS,CAAC,SAAS,EAAE,CAAC,CAAC,CAAA;KACpC,OAAO,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,EAAE,SAAS,CAAC,CAAA;KACpC,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.mts","sources":["../src/index.ts"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
function taggedError(tag) {
|
|
2
|
+
return class TaggedErrorClass extends Error {
|
|
3
|
+
constructor(message, options) {
|
|
4
|
+
super(message);
|
|
5
|
+
this._tag = tag;
|
|
6
|
+
this.name = tag;
|
|
7
|
+
this.cause = options?.cause;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
}
|
|
1
11
|
function isSuccess(result) {
|
|
2
12
|
return result[0] === void 0;
|
|
3
13
|
}
|
|
@@ -116,25 +126,31 @@ function goTry(value) {
|
|
|
116
126
|
return failure(getErrorMessage(err));
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
|
-
function goTryRaw(value) {
|
|
129
|
+
function goTryRaw(value, ErrorClass) {
|
|
130
|
+
const wrapError = (err) => {
|
|
131
|
+
if (ErrorClass) {
|
|
132
|
+
if (err === void 0) {
|
|
133
|
+
return new ErrorClass("undefined");
|
|
134
|
+
}
|
|
135
|
+
if (isError(err)) {
|
|
136
|
+
return new ErrorClass(err.message, { cause: err });
|
|
137
|
+
}
|
|
138
|
+
return new ErrorClass(String(err));
|
|
139
|
+
}
|
|
140
|
+
if (err === void 0) {
|
|
141
|
+
return new Error("undefined");
|
|
142
|
+
}
|
|
143
|
+
return isError(err) ? err : new Error(String(err));
|
|
144
|
+
};
|
|
120
145
|
try {
|
|
121
146
|
const result = typeof value === "function" ? value() : value;
|
|
122
147
|
if (isPromise(result)) {
|
|
123
|
-
return result.then((resolvedValue) => success(resolvedValue)).catch((err) =>
|
|
124
|
-
if (err === void 0) {
|
|
125
|
-
return failure(new Error("undefined"));
|
|
126
|
-
}
|
|
127
|
-
return failure(
|
|
128
|
-
isError(err) ? err : new Error(String(err))
|
|
129
|
-
);
|
|
130
|
-
});
|
|
148
|
+
return result.then((resolvedValue) => success(resolvedValue)).catch((err) => failure(wrapError(err)));
|
|
131
149
|
}
|
|
132
150
|
return success(result);
|
|
133
151
|
} catch (err) {
|
|
134
|
-
return failure(
|
|
135
|
-
isError(err) ? err : new Error(String(err))
|
|
136
|
-
);
|
|
152
|
+
return failure(wrapError(err));
|
|
137
153
|
}
|
|
138
154
|
}
|
|
139
155
|
|
|
140
|
-
export { failure, goTry, goTryAll, goTryAllRaw, goTryOr, goTryRaw, isFailure, isSuccess, success };
|
|
156
|
+
export { failure, goTry, goTryAll, goTryAllRaw, goTryOr, goTryRaw, isFailure, isSuccess, success, taggedError };
|
package/package.json
CHANGED
package/src/index.test.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
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
|
+
}
|
|
3
9
|
import {
|
|
4
10
|
type Result,
|
|
11
|
+
type TaggedUnion,
|
|
12
|
+
failure,
|
|
5
13
|
goTry,
|
|
6
14
|
goTryAll,
|
|
7
15
|
goTryAllRaw,
|
|
@@ -9,6 +17,8 @@ import {
|
|
|
9
17
|
goTryRaw,
|
|
10
18
|
isFailure,
|
|
11
19
|
isSuccess,
|
|
20
|
+
success,
|
|
21
|
+
taggedError,
|
|
12
22
|
} from './index.js'
|
|
13
23
|
|
|
14
24
|
test(`value returned by callback is used when callback doesn't throw`, async () => {
|
|
@@ -944,3 +954,399 @@ describe('goTryAllRaw', () => {
|
|
|
944
954
|
assert.deepEqual(results, [])
|
|
945
955
|
})
|
|
946
956
|
})
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
describe('taggedError', () => {
|
|
960
|
+
test('creates error class with _tag property', () => {
|
|
961
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
962
|
+
const err = new DatabaseError('connection failed')
|
|
963
|
+
|
|
964
|
+
assert.equal(err._tag, 'DatabaseError')
|
|
965
|
+
assert.equal(err.message, 'connection failed')
|
|
966
|
+
assert.equal(err.name, 'DatabaseError')
|
|
967
|
+
assert.ok(err instanceof Error)
|
|
968
|
+
assert.ok(err instanceof DatabaseError)
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
test('supports cause option', () => {
|
|
972
|
+
const NetworkError = taggedError('NetworkError')
|
|
973
|
+
const cause = new Error('ECONNREFUSED')
|
|
974
|
+
const err = new NetworkError('request failed', { cause })
|
|
975
|
+
|
|
976
|
+
assert.equal(err._tag, 'NetworkError')
|
|
977
|
+
assert.equal(err.message, 'request failed')
|
|
978
|
+
assert.equal(err.cause, cause)
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
test('different error types have distinct _tag values', () => {
|
|
982
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
983
|
+
const NetworkError = taggedError('NetworkError')
|
|
984
|
+
const ValidationError = taggedError('ValidationError')
|
|
985
|
+
|
|
986
|
+
const dbErr = new DatabaseError('db fail')
|
|
987
|
+
const netErr = new NetworkError('net fail')
|
|
988
|
+
const valErr = new ValidationError('val fail')
|
|
989
|
+
|
|
990
|
+
assert.equal(dbErr._tag, 'DatabaseError')
|
|
991
|
+
assert.equal(netErr._tag, 'NetworkError')
|
|
992
|
+
assert.equal(valErr._tag, 'ValidationError')
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
test('works with goTryRaw for discriminated error handling', async () => {
|
|
996
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
997
|
+
const NetworkError = taggedError('NetworkError')
|
|
998
|
+
|
|
999
|
+
// Simulate a database operation that might fail
|
|
1000
|
+
const fetchFromDb = () => {
|
|
1001
|
+
throw new DatabaseError('query failed')
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Simulate a network request that might fail
|
|
1005
|
+
const fetchFromNetwork = () => {
|
|
1006
|
+
throw new NetworkError('timeout')
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Wrap in functions so goTryRaw can catch the errors
|
|
1010
|
+
const [dbErr, dbResult] = goTryRaw(fetchFromDb, DatabaseError)
|
|
1011
|
+
const [netErr, netResult] = goTryRaw(fetchFromNetwork, NetworkError)
|
|
1012
|
+
|
|
1013
|
+
// Type narrowing via discriminated union
|
|
1014
|
+
if (dbErr) {
|
|
1015
|
+
assert.equal(dbErr._tag, 'DatabaseError')
|
|
1016
|
+
assert.equal(dbErr.message, 'query failed')
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (netErr) {
|
|
1020
|
+
assert.equal(netErr._tag, 'NetworkError')
|
|
1021
|
+
assert.equal(netErr.message, 'timeout')
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
assert.equal(dbResult, undefined)
|
|
1025
|
+
assert.equal(netResult, undefined)
|
|
1026
|
+
})
|
|
1027
|
+
|
|
1028
|
+
test('can return union of error types from function', async () => {
|
|
1029
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
1030
|
+
const NetworkError = taggedError('NetworkError')
|
|
1031
|
+
|
|
1032
|
+
type AppError = InstanceType<typeof DatabaseError> | InstanceType<typeof NetworkError>
|
|
1033
|
+
|
|
1034
|
+
async function fetchUser(id: string): Promise<Result<AppError, { id: string; name: string }>> {
|
|
1035
|
+
const [dbErr, user] = await goTryRaw(
|
|
1036
|
+
Promise.resolve({ id, name: 'John' }),
|
|
1037
|
+
DatabaseError,
|
|
1038
|
+
)
|
|
1039
|
+
if (dbErr) return failure<AppError>(dbErr)
|
|
1040
|
+
return [undefined, user] as const
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async function fetchData(): Promise<Result<AppError, string>> {
|
|
1044
|
+
const [netErr, data] = await goTryRaw(Promise.resolve('data'), NetworkError)
|
|
1045
|
+
if (netErr) return failure<AppError>(netErr)
|
|
1046
|
+
return [undefined, data] as const
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Use the functions
|
|
1050
|
+
const [userErr, user] = await fetchUser('123')
|
|
1051
|
+
const [, data] = await fetchData()
|
|
1052
|
+
|
|
1053
|
+
// Type narrowing works with discriminated unions
|
|
1054
|
+
if (userErr) {
|
|
1055
|
+
// userErr._tag can be 'DatabaseError' | 'NetworkError'
|
|
1056
|
+
assert.ok(userErr._tag === 'DatabaseError' || userErr._tag === 'NetworkError')
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
assert.deepEqual(user, { id: '123', name: 'John' })
|
|
1060
|
+
assert.equal(data, 'data')
|
|
1061
|
+
})
|
|
1062
|
+
|
|
1063
|
+
test('pattern matching on error types', async () => {
|
|
1064
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
1065
|
+
const NetworkError = taggedError('NetworkError')
|
|
1066
|
+
const ValidationError = taggedError('ValidationError')
|
|
1067
|
+
|
|
1068
|
+
type AppError =
|
|
1069
|
+
| InstanceType<typeof DatabaseError>
|
|
1070
|
+
| InstanceType<typeof NetworkError>
|
|
1071
|
+
| InstanceType<typeof ValidationError>
|
|
1072
|
+
|
|
1073
|
+
function handleError(err: AppError): string {
|
|
1074
|
+
switch (err._tag) {
|
|
1075
|
+
case 'DatabaseError':
|
|
1076
|
+
return `DB: ${err.message}`
|
|
1077
|
+
case 'NetworkError':
|
|
1078
|
+
return `NET: ${err.message}`
|
|
1079
|
+
case 'ValidationError':
|
|
1080
|
+
return `VAL: ${err.message}`
|
|
1081
|
+
default:
|
|
1082
|
+
return `UNK: ${err.message}`
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
assert.equal(handleError(new DatabaseError('fail')), 'DB: fail')
|
|
1087
|
+
assert.equal(handleError(new NetworkError('timeout')), 'NET: timeout')
|
|
1088
|
+
assert.equal(handleError(new ValidationError('invalid')), 'VAL: invalid')
|
|
1089
|
+
})
|
|
1090
|
+
|
|
1091
|
+
test('exhaustive switch with never type', () => {
|
|
1092
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
1093
|
+
const NetworkError = taggedError('NetworkError')
|
|
1094
|
+
const ValidationError = taggedError('ValidationError')
|
|
1095
|
+
|
|
1096
|
+
type AppError = TaggedUnion<[typeof DatabaseError, typeof NetworkError, typeof ValidationError]>
|
|
1097
|
+
|
|
1098
|
+
// Exhaustive switch - TypeScript will error if any case is missing
|
|
1099
|
+
function handleErrorExhaustive(err: AppError): string {
|
|
1100
|
+
switch (err._tag) {
|
|
1101
|
+
case 'DatabaseError':
|
|
1102
|
+
return `DB: ${err.message}`
|
|
1103
|
+
case 'NetworkError':
|
|
1104
|
+
return `NET: ${err.message}`
|
|
1105
|
+
case 'ValidationError':
|
|
1106
|
+
return `VAL: ${err.message}`
|
|
1107
|
+
default:
|
|
1108
|
+
// This ensures all cases are handled - if we forget a case above,
|
|
1109
|
+
// err will not be never and TypeScript will error
|
|
1110
|
+
return assertNever(err)
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
assert.equal(handleErrorExhaustive(new DatabaseError('fail')), 'DB: fail')
|
|
1115
|
+
assert.equal(handleErrorExhaustive(new NetworkError('timeout')), 'NET: timeout')
|
|
1116
|
+
assert.equal(handleErrorExhaustive(new ValidationError('invalid')), 'VAL: invalid')
|
|
1117
|
+
})
|
|
1118
|
+
})
|
|
1119
|
+
|
|
1120
|
+
describe('taggedError type tests', () => {
|
|
1121
|
+
test('error class has correct type structure', () => {
|
|
1122
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
1123
|
+
const err = new DatabaseError('fail')
|
|
1124
|
+
|
|
1125
|
+
// The error should have _tag, message, and cause properties
|
|
1126
|
+
attest<'DatabaseError'>(err._tag)
|
|
1127
|
+
attest<string>(err.message)
|
|
1128
|
+
attest<unknown | undefined>(err.cause)
|
|
1129
|
+
// Error instance check - the class extends Error
|
|
1130
|
+
assert.ok(err instanceof Error)
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
test('discriminated union type narrowing works', () => {
|
|
1134
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
1135
|
+
const NetworkError = taggedError('NetworkError')
|
|
1136
|
+
|
|
1137
|
+
type AppError = InstanceType<typeof DatabaseError> | InstanceType<typeof NetworkError>
|
|
1138
|
+
|
|
1139
|
+
const err: AppError = new DatabaseError('fail')
|
|
1140
|
+
|
|
1141
|
+
// Type narrowing via _tag
|
|
1142
|
+
if (err._tag === 'DatabaseError') {
|
|
1143
|
+
attest<InstanceType<typeof DatabaseError>>(err)
|
|
1144
|
+
} else {
|
|
1145
|
+
attest<InstanceType<typeof NetworkError>>(err)
|
|
1146
|
+
}
|
|
1147
|
+
})
|
|
1148
|
+
})
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
describe('TaggedUnion type helper', () => {
|
|
1152
|
+
test('creates union type from multiple error classes', () => {
|
|
1153
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
1154
|
+
const NetworkError = taggedError('NetworkError')
|
|
1155
|
+
const ValidationError = taggedError('ValidationError')
|
|
1156
|
+
|
|
1157
|
+
// Create union type using helper
|
|
1158
|
+
type AppError = TaggedUnion<[typeof DatabaseError, typeof NetworkError, typeof ValidationError]>
|
|
1159
|
+
|
|
1160
|
+
// All error types should be assignable to AppError
|
|
1161
|
+
const dbErr: AppError = new DatabaseError('db fail')
|
|
1162
|
+
const netErr: AppError = new NetworkError('net fail')
|
|
1163
|
+
const valErr: AppError = new ValidationError('val fail')
|
|
1164
|
+
|
|
1165
|
+
assert.equal(dbErr._tag, 'DatabaseError')
|
|
1166
|
+
assert.equal(netErr._tag, 'NetworkError')
|
|
1167
|
+
assert.equal(valErr._tag, 'ValidationError')
|
|
1168
|
+
|
|
1169
|
+
// Pattern matching should work
|
|
1170
|
+
function handleError(err: AppError): string {
|
|
1171
|
+
switch (err._tag) {
|
|
1172
|
+
case 'DatabaseError':
|
|
1173
|
+
return `DB: ${err.message}`
|
|
1174
|
+
case 'NetworkError':
|
|
1175
|
+
return `NET: ${err.message}`
|
|
1176
|
+
case 'ValidationError':
|
|
1177
|
+
return `VAL: ${err.message}`
|
|
1178
|
+
default:
|
|
1179
|
+
return `UNK: ${err.message}`
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
assert.equal(handleError(dbErr), 'DB: db fail')
|
|
1184
|
+
assert.equal(handleError(netErr), 'NET: net fail')
|
|
1185
|
+
assert.equal(handleError(valErr), 'VAL: val fail')
|
|
1186
|
+
})
|
|
1187
|
+
|
|
1188
|
+
test('works with goTryRaw for typed error handling', async () => {
|
|
1189
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
1190
|
+
const NetworkError = taggedError('NetworkError')
|
|
1191
|
+
|
|
1192
|
+
type AppError = TaggedUnion<[typeof DatabaseError, typeof NetworkError]>
|
|
1193
|
+
|
|
1194
|
+
async function fetchData(): Promise<Result<AppError, string>> {
|
|
1195
|
+
const [err, data] = await goTryRaw(
|
|
1196
|
+
Promise.reject(new Error('timeout')),
|
|
1197
|
+
NetworkError,
|
|
1198
|
+
)
|
|
1199
|
+
if (err) return failure<AppError>(err)
|
|
1200
|
+
return [undefined, data] as const
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const [err, data] = await fetchData()
|
|
1204
|
+
assert.equal(data, undefined)
|
|
1205
|
+
assert.equal(err?._tag, 'NetworkError')
|
|
1206
|
+
assert.equal(err?.message, 'timeout')
|
|
1207
|
+
})
|
|
1208
|
+
})
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
describe('inferred return types with tagged errors', () => {
|
|
1212
|
+
test('function infers union return type without explicit annotation', async () => {
|
|
1213
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
1214
|
+
const NetworkError = taggedError('NetworkError')
|
|
1215
|
+
|
|
1216
|
+
// No explicit return type - TypeScript should infer it
|
|
1217
|
+
async function fetchUserData(id: string) {
|
|
1218
|
+
// First operation might fail with DatabaseError
|
|
1219
|
+
const [dbErr, user] = await goTryRaw(
|
|
1220
|
+
Promise.resolve({ id, name: 'John' }),
|
|
1221
|
+
DatabaseError,
|
|
1222
|
+
)
|
|
1223
|
+
if (dbErr) return failure(dbErr)
|
|
1224
|
+
|
|
1225
|
+
// Second operation might fail with NetworkError
|
|
1226
|
+
const [netErr, enriched] = await goTryRaw(
|
|
1227
|
+
Promise.resolve({ ...user!, email: 'john@example.com' }),
|
|
1228
|
+
NetworkError,
|
|
1229
|
+
)
|
|
1230
|
+
if (netErr) return failure(netErr)
|
|
1231
|
+
|
|
1232
|
+
return [undefined, enriched] as const
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// The return type should be inferred as:
|
|
1236
|
+
// Promise<Result<DatabaseError | NetworkError, { id: string; name: string; email: string }>>
|
|
1237
|
+
|
|
1238
|
+
const [err, user] = await fetchUserData('123')
|
|
1239
|
+
|
|
1240
|
+
// Type narrowing should work
|
|
1241
|
+
if (err) {
|
|
1242
|
+
// err should be DatabaseError | NetworkError
|
|
1243
|
+
switch (err._tag) {
|
|
1244
|
+
case 'DatabaseError':
|
|
1245
|
+
assert.equal(err._tag, 'DatabaseError')
|
|
1246
|
+
break
|
|
1247
|
+
case 'NetworkError':
|
|
1248
|
+
assert.equal(err._tag, 'NetworkError')
|
|
1249
|
+
break
|
|
1250
|
+
default:
|
|
1251
|
+
assertNever(err)
|
|
1252
|
+
}
|
|
1253
|
+
} else {
|
|
1254
|
+
// user should be fully typed
|
|
1255
|
+
assert.deepEqual(user, { id: '123', name: 'John', email: 'john@example.com' })
|
|
1256
|
+
}
|
|
1257
|
+
})
|
|
1258
|
+
|
|
1259
|
+
test('sync function infers union return type', () => {
|
|
1260
|
+
const ParseError = taggedError('ParseError')
|
|
1261
|
+
const ValidateError = taggedError('ValidateError')
|
|
1262
|
+
|
|
1263
|
+
// No explicit return type annotation
|
|
1264
|
+
function processConfig(input: string) {
|
|
1265
|
+
// Parse step
|
|
1266
|
+
const [parseErr, parsed] = goTryRaw(() => JSON.parse(input), ParseError)
|
|
1267
|
+
if (parseErr) return failure(parseErr)
|
|
1268
|
+
|
|
1269
|
+
// Validate step
|
|
1270
|
+
const [validateErr, validated] = goTryRaw(() => {
|
|
1271
|
+
if (!parsed!.port) throw new Error('Missing port')
|
|
1272
|
+
return parsed as { port: number }
|
|
1273
|
+
}, ValidateError)
|
|
1274
|
+
if (validateErr) return failure(validateErr)
|
|
1275
|
+
|
|
1276
|
+
return [undefined, validated] as const
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Success case
|
|
1280
|
+
const [err1, config1] = processConfig('{"port": 3000}')
|
|
1281
|
+
assert.equal(err1, undefined)
|
|
1282
|
+
assert.deepEqual(config1, { port: 3000 })
|
|
1283
|
+
|
|
1284
|
+
// Parse error case
|
|
1285
|
+
const [err2, config2] = processConfig('invalid json')
|
|
1286
|
+
assert.equal(err2?._tag, 'ParseError')
|
|
1287
|
+
assert.equal(config2, undefined)
|
|
1288
|
+
|
|
1289
|
+
// Validation error case
|
|
1290
|
+
const [err3, config3] = processConfig('{"host": "localhost"}')
|
|
1291
|
+
assert.equal(err3?._tag, 'ValidateError')
|
|
1292
|
+
assert.equal(config3, undefined)
|
|
1293
|
+
})
|
|
1294
|
+
|
|
1295
|
+
test('multiple error sources collapse to union type', async () => {
|
|
1296
|
+
const ErrorA = taggedError('ErrorA')
|
|
1297
|
+
const ErrorB = taggedError('ErrorB')
|
|
1298
|
+
const ErrorC = taggedError('ErrorC')
|
|
1299
|
+
|
|
1300
|
+
// Function with multiple potential error sources
|
|
1301
|
+
async function complexOperation(shouldFail: 'a' | 'b' | 'c' | 'none') {
|
|
1302
|
+
const [errA, valA] = await goTryRaw(
|
|
1303
|
+
shouldFail === 'a' ? Promise.reject(new Error('a')) : Promise.resolve('step1'),
|
|
1304
|
+
ErrorA,
|
|
1305
|
+
)
|
|
1306
|
+
if (errA) return failure(errA)
|
|
1307
|
+
|
|
1308
|
+
const [errB, valB] = await goTryRaw(
|
|
1309
|
+
shouldFail === 'b' ? Promise.reject(new Error('b')) : Promise.resolve('step2'),
|
|
1310
|
+
ErrorB,
|
|
1311
|
+
)
|
|
1312
|
+
if (errB) return failure(errB)
|
|
1313
|
+
|
|
1314
|
+
const [errC, valC] = await goTryRaw(
|
|
1315
|
+
shouldFail === 'c' ? Promise.reject(new Error('c')) : Promise.resolve('step3'),
|
|
1316
|
+
ErrorC,
|
|
1317
|
+
)
|
|
1318
|
+
if (errC) return failure(errC)
|
|
1319
|
+
|
|
1320
|
+
return success({ valA, valB, valC })
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// All cases should be handled with exhaustive switch
|
|
1324
|
+
async function handleOperation(shouldFail: 'a' | 'b' | 'c' | 'none') {
|
|
1325
|
+
const [err, result] = await complexOperation(shouldFail)
|
|
1326
|
+
|
|
1327
|
+
if (err) {
|
|
1328
|
+
// TypeScript should infer err as ErrorA | ErrorB | ErrorC
|
|
1329
|
+
switch (err._tag) {
|
|
1330
|
+
case 'ErrorA':
|
|
1331
|
+
return `Failed at step A: ${err.message}`
|
|
1332
|
+
case 'ErrorB':
|
|
1333
|
+
return `Failed at step B: ${err.message}`
|
|
1334
|
+
case 'ErrorC':
|
|
1335
|
+
return `Failed at step C: ${err.message}`
|
|
1336
|
+
default:
|
|
1337
|
+
return assertNever(err)
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
return `Success: ${JSON.stringify(result)}`
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
assert.equal(await handleOperation('a'), 'Failed at step A: a')
|
|
1345
|
+
assert.equal(await handleOperation('b'), 'Failed at step B: b')
|
|
1346
|
+
assert.equal(await handleOperation('c'), 'Failed at step C: c')
|
|
1347
|
+
assert.equal(
|
|
1348
|
+
await handleOperation('none'),
|
|
1349
|
+
'Success: {"valA":"step1","valB":"step2","valC":"step3"}',
|
|
1350
|
+
)
|
|
1351
|
+
})
|
|
1352
|
+
})
|