errore 0.5.1 → 0.6.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/README.md CHANGED
@@ -22,10 +22,10 @@ npm install errore
22
22
  ## Quick Start
23
23
 
24
24
  ```ts
25
- import { tryAsync, isError, TaggedError, matchError } from 'errore'
25
+ import * as errore from 'errore'
26
26
 
27
27
  // Define typed errors
28
- class NotFoundError extends TaggedError('NotFoundError')<{
28
+ class NotFoundError extends errore.TaggedError('NotFoundError')<{
29
29
  id: string
30
30
  message: string
31
31
  }>() {
@@ -34,19 +34,19 @@ class NotFoundError extends TaggedError('NotFoundError')<{
34
34
  }
35
35
  }
36
36
 
37
- class DbError extends TaggedError('DbError')<{
37
+ class DbError extends errore.TaggedError('DbError')<{
38
38
  message: string
39
39
  cause: unknown
40
40
  }>() {}
41
41
 
42
42
  // Function returns Error | Value (no wrapper!)
43
43
  async function getUser(id: string): Promise<NotFoundError | DbError | User> {
44
- const result = await tryAsync({
44
+ const result = await errore.tryAsync({
45
45
  try: () => db.query(id),
46
46
  catch: e => new DbError({ message: 'Query failed', cause: e })
47
47
  })
48
48
 
49
- if (isError(result)) return result
49
+ if (errore.isError(result)) return result
50
50
  if (!result) return new NotFoundError({ id })
51
51
 
52
52
  return result
@@ -55,8 +55,8 @@ async function getUser(id: string): Promise<NotFoundError | DbError | User> {
55
55
  // Caller handles errors explicitly
56
56
  const user = await getUser('123')
57
57
 
58
- if (isError(user)) {
59
- matchError(user, {
58
+ if (errore.isError(user)) {
59
+ errore.matchError(user, {
60
60
  NotFoundError: e => console.log(`User ${e.id} not found`),
61
61
  DbError: e => console.log(`Database error: ${e.message}`)
62
62
  })
@@ -72,11 +72,11 @@ console.log(user.name)
72
72
  ### Type Guards
73
73
 
74
74
  ```ts
75
- import { isError, isOk } from 'errore'
75
+ import * as errore from 'errore'
76
76
 
77
77
  const result: NetworkError | User = await fetchUser(id)
78
78
 
79
- if (isError(result)) {
79
+ if (errore.isError(result)) {
80
80
  // result is NetworkError
81
81
  return result
82
82
  }
@@ -86,22 +86,22 @@ if (isError(result)) {
86
86
  ### Try Functions
87
87
 
88
88
  ```ts
89
- import { tryFn, tryAsync } from 'errore'
89
+ import * as errore from 'errore'
90
90
 
91
91
  // Sync - wraps exceptions in UnhandledError
92
- const parsed = tryFn(() => JSON.parse(input))
92
+ const parsed = errore.tryFn(() => JSON.parse(input))
93
93
 
94
94
  // Sync - with custom error type
95
- const parsed = tryFn({
95
+ const parsed = errore.tryFn({
96
96
  try: () => JSON.parse(input),
97
97
  catch: e => new ParseError({ cause: e })
98
98
  })
99
99
 
100
100
  // Async
101
- const response = await tryAsync(() => fetch(url))
101
+ const response = await errore.tryAsync(() => fetch(url))
102
102
 
103
103
  // Async - with custom error
104
- const response = await tryAsync({
104
+ const response = await errore.tryAsync({
105
105
  try: () => fetch(url),
106
106
  catch: e => new NetworkError({ cause: e })
107
107
  })
@@ -110,19 +110,19 @@ const response = await tryAsync({
110
110
  ### Transformations
111
111
 
112
112
  ```ts
113
- import { map, mapError, andThen, tap } from 'errore'
113
+ import * as errore from 'errore'
114
114
 
115
115
  // Transform value (if not error)
116
- const name = map(user, u => u.name)
116
+ const name = errore.map(user, u => u.name)
117
117
 
118
118
  // Transform error
119
- const appError = mapError(dbError, e => new AppError({ cause: e }))
119
+ const appError = errore.mapError(dbError, e => new AppError({ cause: e }))
120
120
 
121
121
  // Chain operations
122
- const posts = andThen(user, u => fetchPosts(u.id))
122
+ const posts = errore.andThen(user, u => fetchPosts(u.id))
123
123
 
124
124
  // Side effects
125
- const logged = tap(user, u => console.log('Got user:', u.name))
125
+ const logged = errore.tap(user, u => console.log('Got user:', u.name))
126
126
  ```
127
127
 
128
128
  ### Composing Operations
@@ -130,7 +130,7 @@ const logged = tap(user, u => console.log('Got user:', u.name))
130
130
  Chain multiple operations together:
131
131
 
132
132
  ```ts
133
- import { map, andThen, mapError, isError } from 'errore'
133
+ import * as errore from 'errore'
134
134
 
135
135
  // Define operations that can fail
136
136
  function parseNumber(s: string): ValidationError | number { ... }
@@ -138,24 +138,24 @@ function validatePositive(n: number): ValidationError | number { ... }
138
138
  function divide(a: number, b: number): DivisionError | number { ... }
139
139
 
140
140
  // Compose with nested calls
141
- const result = andThen(
142
- andThen(parseNumber(input), validatePositive),
141
+ const result = errore.andThen(
142
+ errore.andThen(parseNumber(input), validatePositive),
143
143
  n => divide(100, n)
144
144
  )
145
145
 
146
146
  // Or step by step (often clearer)
147
147
  function calculate(input: string): ValidationError | DivisionError | number {
148
148
  const parsed = parseNumber(input)
149
- if (isError(parsed)) return parsed
149
+ if (errore.isError(parsed)) return parsed
150
150
 
151
151
  const validated = validatePositive(parsed)
152
- if (isError(validated)) return validated
152
+ if (errore.isError(validated)) return validated
153
153
 
154
154
  return divide(100, validated)
155
155
  }
156
156
 
157
157
  // Transform errors at the end
158
- const appResult = mapError(
158
+ const appResult = errore.mapError(
159
159
  calculate(userInput),
160
160
  e => new AppError({ source: e._tag, message: e.message })
161
161
  )
@@ -164,23 +164,25 @@ const appResult = mapError(
164
164
  Real-world async composition:
165
165
 
166
166
  ```ts
167
+ import * as errore from 'errore'
168
+
167
169
  async function processOrder(orderId: string): Promise<OrderError | Receipt> {
168
170
  const order = await fetchOrder(orderId)
169
- if (isError(order)) return order
171
+ if (errore.isError(order)) return order
170
172
 
171
173
  const validated = validateOrder(order)
172
- if (isError(validated)) return validated
174
+ if (errore.isError(validated)) return validated
173
175
 
174
176
  const payment = await processPayment(validated)
175
- if (isError(payment)) return payment
177
+ if (errore.isError(payment)) return payment
176
178
 
177
179
  return generateReceipt(payment)
178
180
  }
179
181
 
180
182
  // Caller gets union of all possible errors
181
183
  const receipt = await processOrder('123')
182
- if (isError(receipt)) {
183
- const message = matchError(receipt, {
184
+ if (errore.isError(receipt)) {
185
+ const message = errore.matchError(receipt, {
184
186
  NotFoundError: e => `Order ${e.id} not found`,
185
187
  ValidationError: e => `Invalid: ${e.field}`,
186
188
  PaymentError: e => `Payment failed: ${e.reason}`,
@@ -193,37 +195,37 @@ if (isError(receipt)) {
193
195
  ### Extraction
194
196
 
195
197
  ```ts
196
- import { unwrap, unwrapOr, match, partition } from 'errore'
198
+ import * as errore from 'errore'
197
199
 
198
200
  // Extract or throw
199
- const user = unwrap(result)
200
- const user = unwrap(result, 'Custom error message')
201
+ const user = errore.unwrap(result)
202
+ const user = errore.unwrap(result, 'Custom error message')
201
203
 
202
204
  // Extract or fallback
203
- const name = unwrapOr(result, 'Anonymous')
205
+ const name = errore.unwrapOr(result, 'Anonymous')
204
206
 
205
207
  // Pattern match
206
- const message = match(result, {
208
+ const message = errore.match(result, {
207
209
  ok: user => `Hello, ${user.name}`,
208
210
  err: error => `Failed: ${error.message}`
209
211
  })
210
212
 
211
213
  // Split array into [successes, errors]
212
- const [users, errors] = partition(results)
214
+ const [users, errors] = errore.partition(results)
213
215
  ```
214
216
 
215
217
  ### Tagged Errors
216
218
 
217
219
  ```ts
218
- import { TaggedError, matchError, matchErrorPartial } from 'errore'
220
+ import * as errore from 'errore'
219
221
 
220
222
  // Define errors with _tag discriminant
221
- class ValidationError extends TaggedError('ValidationError')<{
223
+ class ValidationError extends errore.TaggedError('ValidationError')<{
222
224
  field: string
223
225
  message: string
224
226
  }>() {}
225
227
 
226
- class NetworkError extends TaggedError('NetworkError')<{
228
+ class NetworkError extends errore.TaggedError('NetworkError')<{
227
229
  url: string
228
230
  message: string
229
231
  }>() {}
@@ -231,7 +233,7 @@ class NetworkError extends TaggedError('NetworkError')<{
231
233
  type AppError = ValidationError | NetworkError
232
234
 
233
235
  // Exhaustive matching (TypeScript ensures all cases handled)
234
- const message = matchError(error, {
236
+ const message = errore.matchError(error, {
235
237
  ValidationError: e => `Invalid ${e.field}`,
236
238
  NetworkError: e => `Failed to fetch ${e.url}`
237
239
  })
@@ -240,20 +242,55 @@ console.log(message)
240
242
  // Handle plain Error with _ (underscore) handler
241
243
  function riskyOp(): ValidationError | Error { ... }
242
244
  const err = riskyOp()
243
- const msg = matchError(err, {
245
+ const msg = errore.matchError(err, {
244
246
  ValidationError: e => `Invalid ${e.field}`,
245
247
  _: e => `Plain error: ${e.message}` // catches non-tagged Error
246
248
  })
247
249
 
248
250
  // Partial matching with fallback
249
- const fallbackMsg = matchErrorPartial(error, {
251
+ const fallbackMsg = errore.matchErrorPartial(error, {
250
252
  ValidationError: e => `Invalid ${e.field}`
251
253
  }, e => `Unknown error: ${e.message}`)
252
254
  console.log(fallbackMsg)
253
255
 
254
256
  // Type guards
255
257
  ValidationError.is(value) // specific class
256
- TaggedError.is(value) // any tagged error
258
+ errore.TaggedError.is(value) // any tagged error
259
+ ```
260
+
261
+ ### Custom Base Class
262
+
263
+ Extend from your own base class to share functionality across all errors:
264
+
265
+ ```ts
266
+ import * as errore from 'errore'
267
+
268
+ // Custom base with shared functionality
269
+ class AppError extends Error {
270
+ statusCode: number = 500
271
+
272
+ report() {
273
+ sentry.captureException(this)
274
+ }
275
+ }
276
+
277
+ // Pass base class as second argument
278
+ class NotFoundError extends errore.TaggedError('NotFoundError', AppError)<{
279
+ id: string
280
+ message: string
281
+ }>() {
282
+ statusCode = 404
283
+ }
284
+
285
+ class ServerError extends errore.TaggedError('ServerError', AppError)<{
286
+ message: string
287
+ }>() {}
288
+
289
+ const err = new NotFoundError({ id: '123', message: 'User not found' })
290
+ err.statusCode // 404
291
+ err.report() // calls sentry
292
+ err._tag // 'NotFoundError'
293
+ err instanceof AppError // true
257
294
  ```
258
295
 
259
296
  ## How Type Safety Works
@@ -283,6 +320,8 @@ One of errore's best features: you can naturally combine error handling with opt
283
320
  In Rust, you'd need `Result<Option<T>, E>` or `Option<Result<T, E>>` and worry about the order. Here it's just a union:
284
321
 
285
322
  ```ts
323
+ import * as errore from 'errore'
324
+
286
325
  // Result + Option in one natural type
287
326
  function findUser(id: string): NotFoundError | User | null {
288
327
  if (id === 'bad') return new NotFoundError({ id })
@@ -293,7 +332,7 @@ function findUser(id: string): NotFoundError | User | null {
293
332
  const user = findUser('123')
294
333
 
295
334
  // Handle error first
296
- if (isError(user)) {
335
+ if (errore.isError(user)) {
297
336
  return user.message // TypeScript: user is NotFoundError
298
337
  }
299
338
 
@@ -312,6 +351,8 @@ console.log(user.name)
312
351
  ### Works with `undefined` too
313
352
 
314
353
  ```ts
354
+ import * as errore from 'errore'
355
+
315
356
  function lookup(key: string): NetworkError | string | undefined {
316
357
  if (key === 'fail') return new NetworkError({ url: '/api', message: 'Failed' })
317
358
  if (key === 'missing') return undefined
@@ -320,7 +361,7 @@ function lookup(key: string): NetworkError | string | undefined {
320
361
 
321
362
  const value = lookup('key')
322
363
 
323
- if (isError(value)) return value
364
+ if (errore.isError(value)) return value
324
365
 
325
366
  // ?? works naturally with undefined
326
367
  const result = value ?? 'default'
@@ -331,6 +372,8 @@ const result = value ?? 'default'
331
372
  Even this works with full type inference:
332
373
 
333
374
  ```ts
375
+ import * as errore from 'errore'
376
+
334
377
  function query(sql: string): ValidationError | { rows: string[] } | null | undefined {
335
378
  if (sql === 'invalid') return new ValidationError({ field: 'sql', message: 'Bad' })
336
379
  if (sql === 'empty') return null // explicitly no data
@@ -340,7 +383,7 @@ function query(sql: string): ValidationError | { rows: string[] } | null | undef
340
383
 
341
384
  const result = query('SELECT *')
342
385
 
343
- if (isError(result)) {
386
+ if (errore.isError(result)) {
344
387
  return result.field // TypeScript: ValidationError
345
388
  }
346
389
 
@@ -377,6 +420,10 @@ With errore:
377
420
  | `Result<User, Error>` | `Error \| User` |
378
421
  | `Result<Option<T>, E>` | `Error \| T \| null` |
379
422
 
423
+ ## Import Style
424
+
425
+ > **Note:** Always use `import * as errore from 'errore'` instead of named imports. This makes code easier to move between files, and more readable for people unfamiliar with errore since every function call is clearly namespaced (e.g. `errore.isError()` instead of just `isError()`).
426
+
380
427
  ## License
381
428
 
382
429
  MIT
package/dist/index.d.mts CHANGED
@@ -25,20 +25,24 @@ type EnsureNotError<T> = T extends Error ? 'Error: Value type T cannot extend Er
25
25
  type AnyTaggedError = Error & {
26
26
  readonly _tag: string;
27
27
  };
28
+ /**
29
+ * Any class that extends Error
30
+ */
31
+ type ErrorClass = new (...args: any[]) => Error;
28
32
  /**
29
33
  * Instance type produced by TaggedError factory
30
34
  */
31
- type TaggedErrorInstance<Tag extends string, Props> = Error & {
35
+ type TaggedErrorInstance<Tag extends string, Props, Base extends Error = Error> = Base & {
32
36
  readonly _tag: Tag;
33
37
  toJSON(): object;
34
38
  } & Readonly<Props>;
35
39
  /**
36
40
  * Class type produced by TaggedError factory
37
41
  */
38
- type TaggedErrorClass<Tag extends string, Props> = {
39
- new (...args: keyof Props extends never ? [args?: {}] : [args: Props]): TaggedErrorInstance<Tag, Props>;
42
+ type TaggedErrorClass<Tag extends string, Props, Base extends Error = Error> = {
43
+ new (...args: keyof Props extends never ? [args?: {}] : [args: Props]): TaggedErrorInstance<Tag, Props, Base>;
40
44
  /** Type guard for this error class */
41
- is(value: unknown): value is TaggedErrorInstance<Tag, Props>;
45
+ is(value: unknown): value is TaggedErrorInstance<Tag, Props, Base>;
42
46
  };
43
47
  /**
44
48
  * Factory for tagged error classes with discriminated _tag property.
@@ -57,9 +61,27 @@ type TaggedErrorClass<Tag extends string, Props> = {
57
61
  * // Type guard
58
62
  * NotFoundError.is(err) // true
59
63
  * TaggedError.is(err) // true (any tagged error)
64
+ *
65
+ * @example
66
+ * // With custom base class
67
+ * class AppError extends Error {
68
+ * statusCode: number = 500
69
+ * report() { console.log(this.message) }
70
+ * }
71
+ *
72
+ * class NotFoundError extends TaggedError("NotFoundError", AppError)<{
73
+ * id: string;
74
+ * message: string;
75
+ * }>() {
76
+ * statusCode = 404
77
+ * }
78
+ *
79
+ * const err = new NotFoundError({ id: "123", message: "Not found" });
80
+ * err.statusCode // 404
81
+ * err.report() // works
60
82
  */
61
83
  declare const TaggedError: {
62
- <Tag extends string>(tag: Tag): <Props extends Record<string, unknown> = {}>() => TaggedErrorClass<Tag, Props>;
84
+ <Tag extends string, BaseClass extends ErrorClass = typeof Error>(tag: Tag, BaseClass?: BaseClass): <Props extends Record<string, unknown> = {}>() => TaggedErrorClass<Tag, Props, InstanceType<BaseClass>>;
63
85
  /** Type guard for any TaggedError instance */
64
86
  is(value: unknown): value is AnyTaggedError;
65
87
  };
@@ -111,7 +133,7 @@ declare function matchErrorPartial<E extends Error, R>(err: E, handlers: Partial
111
133
  declare const UnhandledError_base: TaggedErrorClass<"UnhandledError", {
112
134
  message: string;
113
135
  cause: unknown;
114
- }>;
136
+ }, Error>;
115
137
  /**
116
138
  * Default error type when catching unknown exceptions.
117
139
  */
package/dist/index.d.ts CHANGED
@@ -25,20 +25,24 @@ type EnsureNotError<T> = T extends Error ? 'Error: Value type T cannot extend Er
25
25
  type AnyTaggedError = Error & {
26
26
  readonly _tag: string;
27
27
  };
28
+ /**
29
+ * Any class that extends Error
30
+ */
31
+ type ErrorClass = new (...args: any[]) => Error;
28
32
  /**
29
33
  * Instance type produced by TaggedError factory
30
34
  */
31
- type TaggedErrorInstance<Tag extends string, Props> = Error & {
35
+ type TaggedErrorInstance<Tag extends string, Props, Base extends Error = Error> = Base & {
32
36
  readonly _tag: Tag;
33
37
  toJSON(): object;
34
38
  } & Readonly<Props>;
35
39
  /**
36
40
  * Class type produced by TaggedError factory
37
41
  */
38
- type TaggedErrorClass<Tag extends string, Props> = {
39
- new (...args: keyof Props extends never ? [args?: {}] : [args: Props]): TaggedErrorInstance<Tag, Props>;
42
+ type TaggedErrorClass<Tag extends string, Props, Base extends Error = Error> = {
43
+ new (...args: keyof Props extends never ? [args?: {}] : [args: Props]): TaggedErrorInstance<Tag, Props, Base>;
40
44
  /** Type guard for this error class */
41
- is(value: unknown): value is TaggedErrorInstance<Tag, Props>;
45
+ is(value: unknown): value is TaggedErrorInstance<Tag, Props, Base>;
42
46
  };
43
47
  /**
44
48
  * Factory for tagged error classes with discriminated _tag property.
@@ -57,9 +61,27 @@ type TaggedErrorClass<Tag extends string, Props> = {
57
61
  * // Type guard
58
62
  * NotFoundError.is(err) // true
59
63
  * TaggedError.is(err) // true (any tagged error)
64
+ *
65
+ * @example
66
+ * // With custom base class
67
+ * class AppError extends Error {
68
+ * statusCode: number = 500
69
+ * report() { console.log(this.message) }
70
+ * }
71
+ *
72
+ * class NotFoundError extends TaggedError("NotFoundError", AppError)<{
73
+ * id: string;
74
+ * message: string;
75
+ * }>() {
76
+ * statusCode = 404
77
+ * }
78
+ *
79
+ * const err = new NotFoundError({ id: "123", message: "Not found" });
80
+ * err.statusCode // 404
81
+ * err.report() // works
60
82
  */
61
83
  declare const TaggedError: {
62
- <Tag extends string>(tag: Tag): <Props extends Record<string, unknown> = {}>() => TaggedErrorClass<Tag, Props>;
84
+ <Tag extends string, BaseClass extends ErrorClass = typeof Error>(tag: Tag, BaseClass?: BaseClass): <Props extends Record<string, unknown> = {}>() => TaggedErrorClass<Tag, Props, InstanceType<BaseClass>>;
63
85
  /** Type guard for any TaggedError instance */
64
86
  is(value: unknown): value is AnyTaggedError;
65
87
  };
@@ -111,7 +133,7 @@ declare function matchErrorPartial<E extends Error, R>(err: E, handlers: Partial
111
133
  declare const UnhandledError_base: TaggedErrorClass<"UnhandledError", {
112
134
  message: string;
113
135
  cause: unknown;
114
- }>;
136
+ }, Error>;
115
137
  /**
116
138
  * Default error type when catching unknown exceptions.
117
139
  */
package/dist/index.js CHANGED
@@ -54,12 +54,13 @@ var isAnyTaggedError = (value) => {
54
54
  return value instanceof Error && "_tag" in value && typeof value._tag === "string";
55
55
  };
56
56
  var TaggedError = Object.assign(
57
- (tag) => () => {
58
- class Base extends Error {
57
+ (tag, BaseClass) => () => {
58
+ const ActualBase = BaseClass ?? Error;
59
+ class Tagged extends ActualBase {
59
60
  _tag = tag;
60
61
  /** Type guard for this error class */
61
62
  static is(value) {
62
- return value instanceof Base;
63
+ return value instanceof Tagged;
63
64
  }
64
65
  constructor(args) {
65
66
  const message = args && "message" in args && typeof args.message === "string" ? args.message : void 0;
@@ -87,7 +88,7 @@ Caused by: ${indented}`;
87
88
  };
88
89
  }
89
90
  }
90
- return Base;
91
+ return Tagged;
91
92
  },
92
93
  { is: isAnyTaggedError }
93
94
  );
package/dist/index.mjs CHANGED
@@ -9,12 +9,13 @@ var isAnyTaggedError = (value) => {
9
9
  return value instanceof Error && "_tag" in value && typeof value._tag === "string";
10
10
  };
11
11
  var TaggedError = Object.assign(
12
- (tag) => () => {
13
- class Base extends Error {
12
+ (tag, BaseClass) => () => {
13
+ const ActualBase = BaseClass ?? Error;
14
+ class Tagged extends ActualBase {
14
15
  _tag = tag;
15
16
  /** Type guard for this error class */
16
17
  static is(value) {
17
- return value instanceof Base;
18
+ return value instanceof Tagged;
18
19
  }
19
20
  constructor(args) {
20
21
  const message = args && "message" in args && typeof args.message === "string" ? args.message : void 0;
@@ -42,7 +43,7 @@ Caused by: ${indented}`;
42
43
  };
43
44
  }
44
45
  }
45
- return Base;
46
+ return Tagged;
46
47
  },
47
48
  { is: isAnyTaggedError }
48
49
  );
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "errore",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Type-safe errors as values for TypeScript. Like Go, but with full type inference.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/remorses/errore.git"
8
+ },
9
+ "homepage": "https://github.com/remorses/errore#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/remorses/errore/issues"
12
+ },
5
13
  "main": "dist/index.js",
6
14
  "module": "dist/index.mjs",
7
15
  "types": "dist/index.d.ts",