@crbroughton/failsafe 0.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/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # @crbroughton/failsafe
2
+
3
+ Type-safe error handling utilities for TypeScript. A `Result<T, E>` type
4
+ (inspired by Rust) plus helpers for turning throwing browser/Node APIs into
5
+ values instead of exceptions.
6
+
7
+ Lightweight, dependency-free alternative to neverthrow, plus additional
8
+ helpers for wrapping browser/Node APIs that throw exceptions.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ pnpm add @crbroughton/failsafe
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ ```ts
19
+ import { chain, isOk, match, matchResult, type TaggedError } from "@crbroughton/failsafe"
20
+ import { tryJSONParse, tryLocalStorageGet } from "@crbroughton/failsafe/try"
21
+
22
+ interface User { name: string }
23
+
24
+ const result = chain(
25
+ tryLocalStorageGet("user"),
26
+ raw => raw === null
27
+ ? { ok: false as const, error: { tag: "JSONParseError" as const, raw: "" } }
28
+ : tryJSONParse<User>(raw),
29
+ )
30
+
31
+ if (isOk(result)) {
32
+ console.log(result.value.name)
33
+ }
34
+
35
+ const message = matchResult(result, {
36
+ ok: user => `Welcome, ${user.name}`,
37
+ err: error => `Could not load user: ${error.tag}`,
38
+ })
39
+ ```
40
+
41
+ ## Exports
42
+
43
+ - `@crbroughton/failsafe`: the `Result` type, `Ok`/`Err`, guards, and matchers
44
+ - `@crbroughton/failsafe/try`: `Result`-returning wrappers around throwing web/Node APIs
45
+ - `@crbroughton/failsafe/pipe`: plain left-to-right function composition
46
+
47
+ ## `@crbroughton/failsafe`
48
+
49
+ ### `Result<T, E = Error>`
50
+
51
+ ```ts
52
+ type Result<T, E = Error> = Ok<T> | Err<E>
53
+ interface Ok<T> { ok: true, value: T }
54
+ interface Err<E> { ok: false, error: E }
55
+ ```
56
+
57
+ ### `Ok(value)` / `Err(error)`
58
+
59
+ Construct a `Result` directly.
60
+
61
+ ```ts
62
+ const success = Ok(42) // { ok: true, value: 42 }
63
+ const failure = Err("boom") // { ok: false, error: "boom" }
64
+ ```
65
+
66
+ ### `isOk(result)` / `isErr(result)`
67
+
68
+ Type guards that narrow a `Result` to `Ok<T>` or `Err<E>`.
69
+
70
+ ```ts
71
+ if (isOk(result)) {
72
+ result.value // T
73
+ }
74
+ ```
75
+
76
+ ### `ok(result)` / `err(result)`
77
+
78
+ Extract the value or error as a nullable, for quick one-liners.
79
+
80
+ ```ts
81
+ const value = ok(result) // T | null
82
+ const error = err(result) // E | null
83
+ ```
84
+
85
+ ### `safe(fn | promise, mapError?)`
86
+
87
+ Runs a throwing sync function, or awaits a promise, and returns a `Result`
88
+ instead of throwing/rejecting. Optionally maps the caught value into a typed
89
+ error.
90
+
91
+ ```ts
92
+ // Sync: pass a thunk
93
+ const parsed = safe(() => JSON.parse(raw)) // Result<any, Error>
94
+
95
+ // Async: pass the Promise directly (not a function that returns one)
96
+ const fetched = await safe(fetch("/api/data")) // Result<Response, Error>
97
+
98
+ // With a typed error mapper
99
+ type FetchError = TaggedError<"FetchError", { url: string }>
100
+ const tagged = await safe(
101
+ fetchData(),
102
+ (): FetchError => ({ tag: "FetchError", url: "/api/data" }),
103
+ )
104
+ ```
105
+
106
+ > Passing an async function itself (`safe(async () => ...)`) matches the sync
107
+ > overload, since calling it never throws synchronously; it just returns a
108
+ > Promise. Always pass the awaited Promise directly for async code.
109
+
110
+ ### `chain(result, fn)`
111
+
112
+ Chains a `Result`-returning function onto an existing `Result`,
113
+ short-circuiting on `Err`.
114
+
115
+ ```ts
116
+ const result = chain(tryLocalStorageGet("user"), raw =>
117
+ raw === null ? Err({ tag: "ParseError", raw: "" }) : tryJSONParse<User>(raw))
118
+ ```
119
+
120
+ ### `TaggedError<Tag, Extra?>`
121
+
122
+ A discriminated error shape: every error carries a `tag` so consumers can
123
+ narrow on it.
124
+
125
+ ```ts
126
+ type ParseError = TaggedError<"ParseError", { raw: string }>
127
+ type StorageError = TaggedError<"StorageError", { key: string }>
128
+ ```
129
+
130
+ ### `matchResult(result, { ok, err })`
131
+
132
+ Exhaustively matches a `Result`, requiring both handlers.
133
+
134
+ ```ts
135
+ const message = matchResult(result, {
136
+ ok: value => `Got ${value}`,
137
+ err: error => `Failed: ${error.message}`,
138
+ })
139
+ ```
140
+
141
+ ### `match(taggedError, handlers)`
142
+
143
+ Exhaustively matches a tagged error union. TypeScript requires a handler for
144
+ every tag, and adding a new tag later forces every call site to update.
145
+
146
+ ```ts
147
+ match(result.error, {
148
+ ParseError: e => console.error("bad json:", e.raw),
149
+ StorageError: e => console.error("storage failed:", e.key),
150
+ // omitting a tag here is a compile error
151
+ })
152
+ ```
153
+
154
+ ## `@crbroughton/failsafe/try`
155
+
156
+ `Result`-returning wrappers around browser/Node APIs that throw. Each
157
+ mirrors its native function's signature exactly (including overloaded
158
+ argument types) and returns a `TaggedError` on failure.
159
+
160
+ | Function | Wraps | Error tag |
161
+ | --- | --- | --- |
162
+ | `tryJSONParse<T>(raw, reviver?)` | `JSON.parse` | `JSONParseError` |
163
+ | `tryJSONStringify(value, replacer?, space?)` | `JSON.stringify` | `JSONStringifyError` |
164
+ | `tryURIDecode(input)` | `decodeURIComponent` | `URIError` |
165
+ | `tryURIEncode(input)` | `encodeURIComponent` | `URIError` |
166
+ | `tryBase64Decode(input)` | `atob` | `Base64Error` |
167
+ | `tryBase64Encode(input)` | `btoa` | `Base64Error` |
168
+ | `tryLocalStorageGet(key)` | `localStorage.getItem` | `StorageError` |
169
+ | `tryLocalStorageSet(key, value)` | `localStorage.setItem` | `StorageError` |
170
+ | `tryURL(input, base?)` | `new URL(...)` | `URLParseError` |
171
+ | `tryStructuredClone<T>(value, options?)` | `structuredClone` | `StructuredCloneError` |
172
+
173
+ ```ts
174
+ import { tryJSONParse, tryURL } from "@crbroughton/failsafe/try"
175
+
176
+ const parsed = tryJSONParse<User>(raw)
177
+ const url = tryURL(userSuppliedString)
178
+
179
+ if (isOk(url)) {
180
+ console.log(url.value.hostname)
181
+ }
182
+ ```
183
+
184
+ ## `@crbroughton/failsafe/pipe`
185
+
186
+ ### `pipe(value, ...fns)`
187
+
188
+ Pipes a value through up to five functions, left to right. Deliberately
189
+ independent of `Result`; plain function composition. Compose it with `chain`
190
+ for a `Result` pipeline.
191
+
192
+ ```ts
193
+ import { pipe } from "@crbroughton/failsafe/pipe"
194
+
195
+ const slug = pipe(
196
+ " Hello World ",
197
+ s => s.trim(),
198
+ s => s.toLowerCase(),
199
+ s => s.replace(/\s+/g, "-"),
200
+ )
201
+ // 'hello-world'
202
+ ```
203
+
204
+ ## Development
205
+
206
+ This repo uses [devenv](https://devenv.sh) to provide Node and pnpm.
207
+
208
+ ```sh
209
+ devenv shell
210
+ pnpm install
211
+ pnpm exec nx run-many -t build test lint typecheck
212
+ ```
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pipes a value through a sequence of functions, left to right — each
3
+ * function's output becomes the next function's input.
4
+ *
5
+ * Deliberately independent of Result/safe: this is plain function
6
+ * composition and knows nothing about Ok/Err or short-circuiting. For a
7
+ * pipeline that stops early on an Err, use `chain` from the package root
8
+ * instead — the two compose fine together (see example below).
9
+ *
10
+ * @param value The initial value
11
+ * @returns The result of applying every function in sequence
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const slug = pipe(' Hello World ', (s) => s.trim(), (s) => s.toLowerCase(), (s) => s.replace(/\s+/g, '-'))
16
+ * // 'hello-world'
17
+ * ```
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * // Composing with chain (from '@crbroughton/failsafe') for a Result pipeline:
22
+ * import { chain, Err } from '@crbroughton/failsafe'
23
+ * import { tryLocalStorageGet, tryJSONParse } from '@crbroughton/failsafe/try'
24
+ *
25
+ * const result = pipe(
26
+ * tryLocalStorageGet('user'),
27
+ * (r) => chain(r, (raw) => raw === null ? Err({ tag: 'JSONParseError', raw: '' }) : tryJSONParse<User>(raw)),
28
+ * )
29
+ * ```
30
+ */
31
+ declare function pipe<T>(value: T): T;
32
+ declare function pipe<T, A>(value: T, fn1: (v: T) => A): A;
33
+ declare function pipe<T, A, B>(value: T, fn1: (v: T) => A, fn2: (v: A) => B): B;
34
+ declare function pipe<T, A, B, C>(value: T, fn1: (v: T) => A, fn2: (v: A) => B, fn3: (v: B) => C): C;
35
+ declare function pipe<T, A, B, C, D>(value: T, fn1: (v: T) => A, fn2: (v: A) => B, fn3: (v: B) => C, fn4: (v: C) => D): D;
36
+ declare function pipe<T, A, B, C, D, E>(value: T, fn1: (v: T) => A, fn2: (v: A) => B, fn3: (v: B) => C, fn4: (v: C) => D, fn5: (v: D) => E): E;
37
+
38
+ export { pipe };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pipes a value through a sequence of functions, left to right — each
3
+ * function's output becomes the next function's input.
4
+ *
5
+ * Deliberately independent of Result/safe: this is plain function
6
+ * composition and knows nothing about Ok/Err or short-circuiting. For a
7
+ * pipeline that stops early on an Err, use `chain` from the package root
8
+ * instead — the two compose fine together (see example below).
9
+ *
10
+ * @param value The initial value
11
+ * @returns The result of applying every function in sequence
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const slug = pipe(' Hello World ', (s) => s.trim(), (s) => s.toLowerCase(), (s) => s.replace(/\s+/g, '-'))
16
+ * // 'hello-world'
17
+ * ```
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * // Composing with chain (from '@crbroughton/failsafe') for a Result pipeline:
22
+ * import { chain, Err } from '@crbroughton/failsafe'
23
+ * import { tryLocalStorageGet, tryJSONParse } from '@crbroughton/failsafe/try'
24
+ *
25
+ * const result = pipe(
26
+ * tryLocalStorageGet('user'),
27
+ * (r) => chain(r, (raw) => raw === null ? Err({ tag: 'JSONParseError', raw: '' }) : tryJSONParse<User>(raw)),
28
+ * )
29
+ * ```
30
+ */
31
+ declare function pipe<T>(value: T): T;
32
+ declare function pipe<T, A>(value: T, fn1: (v: T) => A): A;
33
+ declare function pipe<T, A, B>(value: T, fn1: (v: T) => A, fn2: (v: A) => B): B;
34
+ declare function pipe<T, A, B, C>(value: T, fn1: (v: T) => A, fn2: (v: A) => B, fn3: (v: B) => C): C;
35
+ declare function pipe<T, A, B, C, D>(value: T, fn1: (v: T) => A, fn2: (v: A) => B, fn3: (v: B) => C, fn4: (v: C) => D): D;
36
+ declare function pipe<T, A, B, C, D, E>(value: T, fn1: (v: T) => A, fn2: (v: A) => B, fn3: (v: B) => C, fn4: (v: C) => D, fn5: (v: D) => E): E;
37
+
38
+ export { pipe };
@@ -0,0 +1,5 @@
1
+ function pipe(value, ...fns) {
2
+ return fns.reduce((acc, fn) => fn(acc), value);
3
+ }
4
+
5
+ export { pipe };
@@ -0,0 +1,282 @@
1
+ /**
2
+ * A Result type representing either success (Ok) or failure (Err).
3
+ * Inspired by Rust's Result<T, E> type for explicit error handling.
4
+ *
5
+ * @template T The type of the success value
6
+ * @template E The type of the error (defaults to Error)
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const result: Result<number, string> = Ok(42)
11
+ *
12
+ * if (result.ok === false) {
13
+ * console.error(result.error) // TypeScript knows this is string
14
+ * } else {
15
+ * console.log(result.value) // TypeScript knows this is number
16
+ * }
17
+ * ```
18
+ */
19
+ type Result<T, E = Error> = Ok<T> | Err<E>;
20
+ /**
21
+ * Represents a successful result containing a value.
22
+ * @template T The type of the success value
23
+ */
24
+ interface Ok<T> {
25
+ ok: true;
26
+ value: T;
27
+ }
28
+ /**
29
+ * Creates a successful Result with the given value.
30
+ *
31
+ * @template T The type of the value
32
+ * @param value The success value to wrap
33
+ * @returns An Ok result containing the value
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const result = Ok(42)
38
+ * // result is { ok: true, value: 42 }
39
+ * ```
40
+ */
41
+ declare const Ok: <T>(value: T) => Ok<T>;
42
+ /**
43
+ * Represents a failed result containing an error.
44
+ * @template E The type of the error
45
+ */
46
+ interface Err<E> {
47
+ ok: false;
48
+ error: E;
49
+ }
50
+ /**
51
+ * Creates a failed Result with the given error.
52
+ *
53
+ * @template E The type of the error
54
+ * @param error The error to wrap
55
+ * @returns An Err result containing the error
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const result = Err('Something went wrong')
60
+ * // result is { ok: false, error: 'Something went wrong' }
61
+ * ```
62
+ */
63
+ declare const Err: <E>(error: E) => Err<E>;
64
+ /**
65
+ * Type guard to check if a Result is Ok.
66
+ * Narrows the type to Ok<T> when true.
67
+ *
68
+ * @template T The type of the success value
69
+ * @template E The type of the error
70
+ * @param result The Result to check
71
+ * @returns true if the result is Ok, false otherwise
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const result = safe(() => JSON.parse(raw))
76
+ *
77
+ * if (isOk(result)) {
78
+ * // TypeScript knows result is Ok<T>
79
+ * console.log(result.value)
80
+ * }
81
+ * ```
82
+ */
83
+ declare function isOk<T, E>(result: Result<T, E>): result is Ok<T>;
84
+ /**
85
+ * Type guard to check if a Result is Err.
86
+ * Narrows the type to Err<E> when true.
87
+ *
88
+ * @template T The type of the success value
89
+ * @template E The type of the error
90
+ * @param result The Result to check
91
+ * @returns true if the result is Err, false otherwise
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * const result = safe(() => JSON.parse(raw))
96
+ *
97
+ * if (isErr(result)) {
98
+ * // TypeScript knows result is Err<E>
99
+ * console.error(result.error)
100
+ * }
101
+ * ```
102
+ */
103
+ declare function isErr<T, E>(result: Result<T, E>): result is Err<E>;
104
+ /**
105
+ * Extracts the value from a Result if Ok, otherwise returns null.
106
+ * Convenient for converting Result to nullable value.
107
+ *
108
+ * @template T The type of the success value
109
+ * @template E The type of the error
110
+ * @param result The Result to extract from
111
+ * @returns The value if Ok, null if Err
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * const result = safe(() => JSON.parse(raw))
116
+ * const value = ok(result) // T | null
117
+ *
118
+ * const name = ok(result)?.name ?? 'Guest'
119
+ * ```
120
+ */
121
+ declare function ok<T, E>(result: Result<T, E>): T | null;
122
+ /**
123
+ * Extracts the error from a Result if Err, otherwise returns null.
124
+ * Convenient for converting Result to nullable error.
125
+ *
126
+ * @template T The type of the success value
127
+ * @template E The type of the error
128
+ * @param result The Result to extract from
129
+ * @returns The error if Err, null if Ok
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * const result = safe(() => JSON.parse(raw))
134
+ * const error = err(result) // E | null
135
+ *
136
+ * if (error) {
137
+ * console.error('Failed:', error)
138
+ * }
139
+ * ```
140
+ */
141
+ declare function err<T, E>(result: Result<T, E>): E | null;
142
+ /**
143
+ * Runs a throwing SYNCHRONOUS function and returns a Result instead of throwing.
144
+ * Optionally maps the caught error into a typed shape.
145
+ *
146
+ * @template T The return type of the function if it succeeds
147
+ * @template E The error type (defaults to Error)
148
+ * @param fn The synchronous function to run
149
+ * @param mapError Optional — transform the caught value into a typed error
150
+ * @returns Ok(value) or Err(mappedError)
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * const result = safe(() => JSON.parse(raw))
155
+ * // Result<any, Error>
156
+ *
157
+ * type ParseError = TaggedError<'ParseError', { raw: string }>
158
+ *
159
+ * const tagged = safe(
160
+ * () => JSON.parse(raw) as User,
161
+ * (): ParseError => ({ tag: 'ParseError', raw })
162
+ * )
163
+ * // Result<User, ParseError>
164
+ * ```
165
+ */
166
+ declare function safe<T, E = Error>(fn: () => T, mapError?: (error: unknown) => E): Result<T, E>;
167
+ /**
168
+ * Awaits a Promise and returns a Result instead of letting it reject.
169
+ * Optionally maps the caught error into a typed shape.
170
+ *
171
+ * @template T The type of the resolved promise value
172
+ * @template E The error type (defaults to Error)
173
+ * @param promise The promise to await
174
+ * @param mapError Optional — transform the caught value into a typed error
175
+ * @returns A Promise that resolves to Ok(value) or Err(mappedError)
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * const result = await safe(fetch('/api/data'))
180
+ * if (result.ok === false) {
181
+ * console.error('Fetch failed:', result.error)
182
+ * return
183
+ * }
184
+ * const response = result.value
185
+ *
186
+ * type FetchError = TaggedError<'FetchError', { url: string }>
187
+ *
188
+ * const tagged = await safe(
189
+ * fetchData(),
190
+ * (): FetchError => ({ tag: 'FetchError', url })
191
+ * )
192
+ * ```
193
+ */
194
+ declare function safe<T, E = Error>(promise: Promise<T>, mapError?: (error: unknown) => E): Promise<Result<T, E>>;
195
+ /**
196
+ * Chains a Result-returning function onto an existing Result,
197
+ * short-circuiting if the input is already an Err.
198
+ *
199
+ * @template T The input success type
200
+ * @template U The output success type
201
+ * @template E The error type (shared between input and output)
202
+ * @param result The Result to chain from
203
+ * @param fn A function that takes the unwrapped Ok value and returns a new Result
204
+ * @returns The result of fn if input was Ok, otherwise the original Err unchanged
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * const result = chain(tryLocalStorageGet('user'), (raw) =>
209
+ * raw === null ? Err({ tag: 'ParseError', raw: '' }) : tryJSONParse<User>(raw))
210
+ * ```
211
+ */
212
+ declare function chain<T, U, E>(result: Result<T, E>, fn: (value: T) => Result<U, E>): Result<U, E>;
213
+ /**
214
+ * A discriminated error shape — every error carries a `tag` so consumers
215
+ * can narrow on it (if/switch, or match for exhaustive handling).
216
+ *
217
+ * @template Tag The string literal discriminant
218
+ * @template Extra Additional fields specific to this error
219
+ *
220
+ * @example
221
+ * ```ts
222
+ * type ParseError = TaggedError<'ParseError', { raw: string }>
223
+ * type StorageError = TaggedError<'StorageError', { key: string }>
224
+ * ```
225
+ */
226
+ type TaggedError<Tag extends string, Extra extends object = object> = {
227
+ tag: Tag;
228
+ } & Extra;
229
+ /**
230
+ * Exhaustively matches a Result — requires both an ok and an err handler.
231
+ *
232
+ * @template T The success type
233
+ * @template E The error type
234
+ * @template R The return type of both handlers
235
+ * @param result The Result to match on
236
+ * @param handlers Handlers for the ok and err cases
237
+ * @param handlers.ok Called with the unwrapped value when result is Ok
238
+ * @param handlers.err Called with the unwrapped error when result is Err
239
+ * @returns Whichever handler's return value applies
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * const message = matchResult(result, {
244
+ * ok: (value) => `Got ${value}`,
245
+ * err: (error) => `Failed: ${error.message}`,
246
+ * })
247
+ * ```
248
+ */
249
+ declare function matchResult<T, E, R>(result: Result<T, E>, handlers: {
250
+ ok: (value: T) => R;
251
+ err: (error: E) => R;
252
+ }): R;
253
+ /**
254
+ * Exhaustively matches a tagged error union. TypeScript requires a handler
255
+ * for every tag in the union — omitting one is a compile error, and adding
256
+ * a new tag later forces every call site to be updated.
257
+ *
258
+ * @template E The tagged error union type
259
+ * @template Handlers An object with one handler per tag in E
260
+ * @param error The tagged error to match on
261
+ * @param handlers An object mapping each tag to a handler function
262
+ * @returns Whichever handler's return value applies
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * match(result.error, {
267
+ * ParseError: (e) => console.error('bad json:', e.raw),
268
+ * StorageError: (e) => console.error('storage failed:', e.key),
269
+ * // omitting a tag here is a compile error
270
+ * })
271
+ * ```
272
+ */
273
+ declare function match<E extends {
274
+ tag: string;
275
+ }, Handlers extends {
276
+ [K in E["tag"]]: (error: Extract<E, {
277
+ tag: K;
278
+ }>) => unknown;
279
+ }>(error: E, handlers: Handlers): ReturnType<Handlers[E["tag"]]>;
280
+
281
+ export { Err, Ok, chain, err, isErr, isOk, match, matchResult, ok, safe };
282
+ export type { Result, TaggedError };
@@ -0,0 +1,282 @@
1
+ /**
2
+ * A Result type representing either success (Ok) or failure (Err).
3
+ * Inspired by Rust's Result<T, E> type for explicit error handling.
4
+ *
5
+ * @template T The type of the success value
6
+ * @template E The type of the error (defaults to Error)
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const result: Result<number, string> = Ok(42)
11
+ *
12
+ * if (result.ok === false) {
13
+ * console.error(result.error) // TypeScript knows this is string
14
+ * } else {
15
+ * console.log(result.value) // TypeScript knows this is number
16
+ * }
17
+ * ```
18
+ */
19
+ type Result<T, E = Error> = Ok<T> | Err<E>;
20
+ /**
21
+ * Represents a successful result containing a value.
22
+ * @template T The type of the success value
23
+ */
24
+ interface Ok<T> {
25
+ ok: true;
26
+ value: T;
27
+ }
28
+ /**
29
+ * Creates a successful Result with the given value.
30
+ *
31
+ * @template T The type of the value
32
+ * @param value The success value to wrap
33
+ * @returns An Ok result containing the value
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const result = Ok(42)
38
+ * // result is { ok: true, value: 42 }
39
+ * ```
40
+ */
41
+ declare const Ok: <T>(value: T) => Ok<T>;
42
+ /**
43
+ * Represents a failed result containing an error.
44
+ * @template E The type of the error
45
+ */
46
+ interface Err<E> {
47
+ ok: false;
48
+ error: E;
49
+ }
50
+ /**
51
+ * Creates a failed Result with the given error.
52
+ *
53
+ * @template E The type of the error
54
+ * @param error The error to wrap
55
+ * @returns An Err result containing the error
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const result = Err('Something went wrong')
60
+ * // result is { ok: false, error: 'Something went wrong' }
61
+ * ```
62
+ */
63
+ declare const Err: <E>(error: E) => Err<E>;
64
+ /**
65
+ * Type guard to check if a Result is Ok.
66
+ * Narrows the type to Ok<T> when true.
67
+ *
68
+ * @template T The type of the success value
69
+ * @template E The type of the error
70
+ * @param result The Result to check
71
+ * @returns true if the result is Ok, false otherwise
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const result = safe(() => JSON.parse(raw))
76
+ *
77
+ * if (isOk(result)) {
78
+ * // TypeScript knows result is Ok<T>
79
+ * console.log(result.value)
80
+ * }
81
+ * ```
82
+ */
83
+ declare function isOk<T, E>(result: Result<T, E>): result is Ok<T>;
84
+ /**
85
+ * Type guard to check if a Result is Err.
86
+ * Narrows the type to Err<E> when true.
87
+ *
88
+ * @template T The type of the success value
89
+ * @template E The type of the error
90
+ * @param result The Result to check
91
+ * @returns true if the result is Err, false otherwise
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * const result = safe(() => JSON.parse(raw))
96
+ *
97
+ * if (isErr(result)) {
98
+ * // TypeScript knows result is Err<E>
99
+ * console.error(result.error)
100
+ * }
101
+ * ```
102
+ */
103
+ declare function isErr<T, E>(result: Result<T, E>): result is Err<E>;
104
+ /**
105
+ * Extracts the value from a Result if Ok, otherwise returns null.
106
+ * Convenient for converting Result to nullable value.
107
+ *
108
+ * @template T The type of the success value
109
+ * @template E The type of the error
110
+ * @param result The Result to extract from
111
+ * @returns The value if Ok, null if Err
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * const result = safe(() => JSON.parse(raw))
116
+ * const value = ok(result) // T | null
117
+ *
118
+ * const name = ok(result)?.name ?? 'Guest'
119
+ * ```
120
+ */
121
+ declare function ok<T, E>(result: Result<T, E>): T | null;
122
+ /**
123
+ * Extracts the error from a Result if Err, otherwise returns null.
124
+ * Convenient for converting Result to nullable error.
125
+ *
126
+ * @template T The type of the success value
127
+ * @template E The type of the error
128
+ * @param result The Result to extract from
129
+ * @returns The error if Err, null if Ok
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * const result = safe(() => JSON.parse(raw))
134
+ * const error = err(result) // E | null
135
+ *
136
+ * if (error) {
137
+ * console.error('Failed:', error)
138
+ * }
139
+ * ```
140
+ */
141
+ declare function err<T, E>(result: Result<T, E>): E | null;
142
+ /**
143
+ * Runs a throwing SYNCHRONOUS function and returns a Result instead of throwing.
144
+ * Optionally maps the caught error into a typed shape.
145
+ *
146
+ * @template T The return type of the function if it succeeds
147
+ * @template E The error type (defaults to Error)
148
+ * @param fn The synchronous function to run
149
+ * @param mapError Optional — transform the caught value into a typed error
150
+ * @returns Ok(value) or Err(mappedError)
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * const result = safe(() => JSON.parse(raw))
155
+ * // Result<any, Error>
156
+ *
157
+ * type ParseError = TaggedError<'ParseError', { raw: string }>
158
+ *
159
+ * const tagged = safe(
160
+ * () => JSON.parse(raw) as User,
161
+ * (): ParseError => ({ tag: 'ParseError', raw })
162
+ * )
163
+ * // Result<User, ParseError>
164
+ * ```
165
+ */
166
+ declare function safe<T, E = Error>(fn: () => T, mapError?: (error: unknown) => E): Result<T, E>;
167
+ /**
168
+ * Awaits a Promise and returns a Result instead of letting it reject.
169
+ * Optionally maps the caught error into a typed shape.
170
+ *
171
+ * @template T The type of the resolved promise value
172
+ * @template E The error type (defaults to Error)
173
+ * @param promise The promise to await
174
+ * @param mapError Optional — transform the caught value into a typed error
175
+ * @returns A Promise that resolves to Ok(value) or Err(mappedError)
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * const result = await safe(fetch('/api/data'))
180
+ * if (result.ok === false) {
181
+ * console.error('Fetch failed:', result.error)
182
+ * return
183
+ * }
184
+ * const response = result.value
185
+ *
186
+ * type FetchError = TaggedError<'FetchError', { url: string }>
187
+ *
188
+ * const tagged = await safe(
189
+ * fetchData(),
190
+ * (): FetchError => ({ tag: 'FetchError', url })
191
+ * )
192
+ * ```
193
+ */
194
+ declare function safe<T, E = Error>(promise: Promise<T>, mapError?: (error: unknown) => E): Promise<Result<T, E>>;
195
+ /**
196
+ * Chains a Result-returning function onto an existing Result,
197
+ * short-circuiting if the input is already an Err.
198
+ *
199
+ * @template T The input success type
200
+ * @template U The output success type
201
+ * @template E The error type (shared between input and output)
202
+ * @param result The Result to chain from
203
+ * @param fn A function that takes the unwrapped Ok value and returns a new Result
204
+ * @returns The result of fn if input was Ok, otherwise the original Err unchanged
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * const result = chain(tryLocalStorageGet('user'), (raw) =>
209
+ * raw === null ? Err({ tag: 'ParseError', raw: '' }) : tryJSONParse<User>(raw))
210
+ * ```
211
+ */
212
+ declare function chain<T, U, E>(result: Result<T, E>, fn: (value: T) => Result<U, E>): Result<U, E>;
213
+ /**
214
+ * A discriminated error shape — every error carries a `tag` so consumers
215
+ * can narrow on it (if/switch, or match for exhaustive handling).
216
+ *
217
+ * @template Tag The string literal discriminant
218
+ * @template Extra Additional fields specific to this error
219
+ *
220
+ * @example
221
+ * ```ts
222
+ * type ParseError = TaggedError<'ParseError', { raw: string }>
223
+ * type StorageError = TaggedError<'StorageError', { key: string }>
224
+ * ```
225
+ */
226
+ type TaggedError<Tag extends string, Extra extends object = object> = {
227
+ tag: Tag;
228
+ } & Extra;
229
+ /**
230
+ * Exhaustively matches a Result — requires both an ok and an err handler.
231
+ *
232
+ * @template T The success type
233
+ * @template E The error type
234
+ * @template R The return type of both handlers
235
+ * @param result The Result to match on
236
+ * @param handlers Handlers for the ok and err cases
237
+ * @param handlers.ok Called with the unwrapped value when result is Ok
238
+ * @param handlers.err Called with the unwrapped error when result is Err
239
+ * @returns Whichever handler's return value applies
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * const message = matchResult(result, {
244
+ * ok: (value) => `Got ${value}`,
245
+ * err: (error) => `Failed: ${error.message}`,
246
+ * })
247
+ * ```
248
+ */
249
+ declare function matchResult<T, E, R>(result: Result<T, E>, handlers: {
250
+ ok: (value: T) => R;
251
+ err: (error: E) => R;
252
+ }): R;
253
+ /**
254
+ * Exhaustively matches a tagged error union. TypeScript requires a handler
255
+ * for every tag in the union — omitting one is a compile error, and adding
256
+ * a new tag later forces every call site to be updated.
257
+ *
258
+ * @template E The tagged error union type
259
+ * @template Handlers An object with one handler per tag in E
260
+ * @param error The tagged error to match on
261
+ * @param handlers An object mapping each tag to a handler function
262
+ * @returns Whichever handler's return value applies
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * match(result.error, {
267
+ * ParseError: (e) => console.error('bad json:', e.raw),
268
+ * StorageError: (e) => console.error('storage failed:', e.key),
269
+ * // omitting a tag here is a compile error
270
+ * })
271
+ * ```
272
+ */
273
+ declare function match<E extends {
274
+ tag: string;
275
+ }, Handlers extends {
276
+ [K in E["tag"]]: (error: Extract<E, {
277
+ tag: K;
278
+ }>) => unknown;
279
+ }>(error: E, handlers: Handlers): ReturnType<Handlers[E["tag"]]>;
280
+
281
+ export { Err, Ok, chain, err, isErr, isOk, match, matchResult, ok, safe };
282
+ export type { Result, TaggedError };
@@ -0,0 +1,35 @@
1
+ const Ok = (value) => ({ ok: true, value });
2
+ const Err = (error) => ({ ok: false, error });
3
+ function isOk(result) {
4
+ return result.ok === true;
5
+ }
6
+ function isErr(result) {
7
+ return result.ok === false;
8
+ }
9
+ function ok(result) {
10
+ return result.ok === true ? result.value : null;
11
+ }
12
+ function err(result) {
13
+ return result.ok === true ? null : result.error;
14
+ }
15
+ function safe(input, mapError) {
16
+ if (input instanceof Promise) {
17
+ return input.then((data) => Ok(data)).catch((error) => Err(mapError ? mapError(error) : error));
18
+ }
19
+ try {
20
+ return Ok(input());
21
+ } catch (error) {
22
+ return Err(mapError ? mapError(error) : error);
23
+ }
24
+ }
25
+ function chain(result, fn) {
26
+ return isOk(result) ? fn(result.value) : result;
27
+ }
28
+ function matchResult(result, handlers) {
29
+ return result.ok ? handlers.ok(result.value) : handlers.err(result.error);
30
+ }
31
+ function match(error, handlers) {
32
+ return handlers[error.tag](error);
33
+ }
34
+
35
+ export { Err, Ok, chain, err, isErr, isOk, match, matchResult, ok, safe };
@@ -0,0 +1,166 @@
1
+ import { TaggedError, Result } from '../safe/index.mjs';
2
+
3
+ /**
4
+ * Error returned when JSON.parse fails on malformed input.
5
+ */
6
+ type JSONParseError = TaggedError<"JSONParseError", {
7
+ raw: string;
8
+ }>;
9
+ /**
10
+ * Error returned when JSON.stringify fails (circular references, BigInt, etc).
11
+ */
12
+ type JSONStringifyError = TaggedError<"JSONStringifyError">;
13
+ /**
14
+ * Error returned when encodeURIComponent/decodeURIComponent fail on a
15
+ * malformed sequence.
16
+ */
17
+ type URIError_ = TaggedError<"URIError", {
18
+ input: string;
19
+ }>;
20
+ /**
21
+ * Error returned when atob/btoa fail on malformed or non-Latin1 input.
22
+ */
23
+ type Base64Error = TaggedError<"Base64Error", {
24
+ input: string;
25
+ }>;
26
+ /**
27
+ * Error returned when a localStorage read/write fails — quota exceeded,
28
+ * storage disabled (e.g. Safari private mode), or serialization failure.
29
+ */
30
+ type StorageError = TaggedError<"StorageError", {
31
+ key: string;
32
+ }>;
33
+ /**
34
+ * Error returned when `new URL(...)` fails on an invalid URL string.
35
+ */
36
+ type URLParseError = TaggedError<"URLParseError", {
37
+ input: string;
38
+ }>;
39
+ /**
40
+ * Error returned when structuredClone fails on a non-cloneable value
41
+ * (functions, DOM nodes, etc).
42
+ */
43
+ type StructuredCloneError = TaggedError<"StructuredCloneError">;
44
+ /**
45
+ * Safely parses a JSON string. Wraps JSON.parse, which throws a
46
+ * SyntaxError on invalid JSON.
47
+ *
48
+ * @template T The expected shape of the parsed value
49
+ * @param raw The JSON string to parse
50
+ * @param reviver Optional JSON.parse reviver, called for each member of the object
51
+ * @returns Ok(parsed value) or Err(JSONParseError)
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * const result = tryJSONParse<User>(raw)
56
+ * if (isOk(result)) {
57
+ * console.log(result.value.name)
58
+ * }
59
+ * ```
60
+ */
61
+ declare function tryJSONParse<T = unknown>(raw: string, reviver?: (this: unknown, key: string, value: unknown) => unknown): Result<T, JSONParseError>;
62
+ /**
63
+ * Safely stringifies a value to JSON. Wraps JSON.stringify, which throws
64
+ * a TypeError on circular references or BigInt values.
65
+ *
66
+ * @param value The value to stringify
67
+ * @param replacer Optional JSON.stringify replacer — a transform function, or an allowlist of keys to include
68
+ * @param space Optional JSON.stringify indentation
69
+ * @returns Ok(json string) or Err(JSONStringifyError)
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const result = tryJSONStringify(data)
74
+ * if (isErr(result)) {
75
+ * console.error('could not serialize:', result.error)
76
+ * }
77
+ * ```
78
+ */
79
+ declare function tryJSONStringify(value: unknown, replacer?: ((this: unknown, key: string, value: unknown) => unknown) | (string | number)[] | null, space?: string | number): Result<string, JSONStringifyError>;
80
+ /**
81
+ * Safely decodes a URI component. Wraps decodeURIComponent, which throws
82
+ * a URIError on malformed percent-encoded sequences.
83
+ *
84
+ * @param input The string to decode
85
+ * @returns Ok(decoded string) or Err(URIError_)
86
+ */
87
+ declare function tryURIDecode(input: string): Result<string, URIError_>;
88
+ /**
89
+ * Safely encodes a URI component. Wraps encodeURIComponent, which throws
90
+ * a URIError on lone surrogate characters.
91
+ *
92
+ * @param input The string, number, or boolean to encode
93
+ * @returns Ok(encoded string) or Err(URIError_)
94
+ */
95
+ declare function tryURIEncode(input: string | number | boolean): Result<string, URIError_>;
96
+ /**
97
+ * Safely decodes a base64 string. Wraps atob, which throws a DOMException
98
+ * on malformed base64 or non-Latin1 input.
99
+ *
100
+ * @param input The base64 string to decode
101
+ * @returns Ok(decoded string) or Err(Base64Error)
102
+ */
103
+ declare function tryBase64Decode(input: string): Result<string, Base64Error>;
104
+ /**
105
+ * Safely encodes a string to base64. Wraps btoa, which throws a
106
+ * DOMException on characters outside the Latin1 range.
107
+ *
108
+ * @param input The string to encode
109
+ * @returns Ok(base64 string) or Err(Base64Error)
110
+ */
111
+ declare function tryBase64Encode(input: string): Result<string, Base64Error>;
112
+ /**
113
+ * Safely reads a value from localStorage. localStorage.getItem doesn't
114
+ * throw on its own, but this exists so a get→parse pipeline stays
115
+ * consistently Result-shaped throughout (pair with tryJSONParse via chain).
116
+ *
117
+ * @param key The storage key to read
118
+ * @returns Ok(value or null) or Err(StorageError)
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * const result = chain(tryLocalStorageGet('user'), (raw) =>
123
+ * raw === null ? Err({ tag: 'JSONParseError', raw: '' }) : tryJSONParse<User>(raw))
124
+ * ```
125
+ */
126
+ declare function tryLocalStorageGet(key: string): Result<string | null, StorageError>;
127
+ /**
128
+ * Safely writes a value to localStorage. Wraps localStorage.setItem, which
129
+ * throws on quota exceeded or when storage is disabled (e.g. Safari
130
+ * private mode).
131
+ *
132
+ * @param key The storage key to write
133
+ * @param value The string value to store
134
+ * @returns Ok(undefined) or Err(StorageError)
135
+ */
136
+ declare function tryLocalStorageSet(key: string, value: string): Result<void, StorageError>;
137
+ /**
138
+ * Safely constructs a URL. Wraps `new URL(...)`, which throws a TypeError
139
+ * on an invalid URL string.
140
+ *
141
+ * @param input The URL (string or URL) to parse
142
+ * @param base Optional base URL (string or URL) to resolve against
143
+ * @returns Ok(URL) or Err(URLParseError)
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * const result = tryURL(userSuppliedString)
148
+ * if (isOk(result)) {
149
+ * console.log(result.value.hostname)
150
+ * }
151
+ * ```
152
+ */
153
+ declare function tryURL(input: string | URL, base?: string | URL): Result<URL, URLParseError>;
154
+ /**
155
+ * Safely deep-clones a value. Wraps structuredClone, which throws a
156
+ * DataCloneError on non-cloneable values (functions, DOM nodes, etc).
157
+ *
158
+ * @template T The type of the value to clone
159
+ * @param value The value to clone
160
+ * @param options Optional structuredClone options (e.g. a transfer list)
161
+ * @returns Ok(cloned value) or Err(StructuredCloneError)
162
+ */
163
+ declare function tryStructuredClone<T>(value: T, options?: StructuredSerializeOptions): Result<T, StructuredCloneError>;
164
+
165
+ export { tryBase64Decode, tryBase64Encode, tryJSONParse, tryJSONStringify, tryLocalStorageGet, tryLocalStorageSet, tryStructuredClone, tryURIDecode, tryURIEncode, tryURL };
166
+ export type { Base64Error, JSONParseError, JSONStringifyError, StorageError, StructuredCloneError, URIError_, URLParseError };
@@ -0,0 +1,166 @@
1
+ import { TaggedError, Result } from '../safe/index.js';
2
+
3
+ /**
4
+ * Error returned when JSON.parse fails on malformed input.
5
+ */
6
+ type JSONParseError = TaggedError<"JSONParseError", {
7
+ raw: string;
8
+ }>;
9
+ /**
10
+ * Error returned when JSON.stringify fails (circular references, BigInt, etc).
11
+ */
12
+ type JSONStringifyError = TaggedError<"JSONStringifyError">;
13
+ /**
14
+ * Error returned when encodeURIComponent/decodeURIComponent fail on a
15
+ * malformed sequence.
16
+ */
17
+ type URIError_ = TaggedError<"URIError", {
18
+ input: string;
19
+ }>;
20
+ /**
21
+ * Error returned when atob/btoa fail on malformed or non-Latin1 input.
22
+ */
23
+ type Base64Error = TaggedError<"Base64Error", {
24
+ input: string;
25
+ }>;
26
+ /**
27
+ * Error returned when a localStorage read/write fails — quota exceeded,
28
+ * storage disabled (e.g. Safari private mode), or serialization failure.
29
+ */
30
+ type StorageError = TaggedError<"StorageError", {
31
+ key: string;
32
+ }>;
33
+ /**
34
+ * Error returned when `new URL(...)` fails on an invalid URL string.
35
+ */
36
+ type URLParseError = TaggedError<"URLParseError", {
37
+ input: string;
38
+ }>;
39
+ /**
40
+ * Error returned when structuredClone fails on a non-cloneable value
41
+ * (functions, DOM nodes, etc).
42
+ */
43
+ type StructuredCloneError = TaggedError<"StructuredCloneError">;
44
+ /**
45
+ * Safely parses a JSON string. Wraps JSON.parse, which throws a
46
+ * SyntaxError on invalid JSON.
47
+ *
48
+ * @template T The expected shape of the parsed value
49
+ * @param raw The JSON string to parse
50
+ * @param reviver Optional JSON.parse reviver, called for each member of the object
51
+ * @returns Ok(parsed value) or Err(JSONParseError)
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * const result = tryJSONParse<User>(raw)
56
+ * if (isOk(result)) {
57
+ * console.log(result.value.name)
58
+ * }
59
+ * ```
60
+ */
61
+ declare function tryJSONParse<T = unknown>(raw: string, reviver?: (this: unknown, key: string, value: unknown) => unknown): Result<T, JSONParseError>;
62
+ /**
63
+ * Safely stringifies a value to JSON. Wraps JSON.stringify, which throws
64
+ * a TypeError on circular references or BigInt values.
65
+ *
66
+ * @param value The value to stringify
67
+ * @param replacer Optional JSON.stringify replacer — a transform function, or an allowlist of keys to include
68
+ * @param space Optional JSON.stringify indentation
69
+ * @returns Ok(json string) or Err(JSONStringifyError)
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const result = tryJSONStringify(data)
74
+ * if (isErr(result)) {
75
+ * console.error('could not serialize:', result.error)
76
+ * }
77
+ * ```
78
+ */
79
+ declare function tryJSONStringify(value: unknown, replacer?: ((this: unknown, key: string, value: unknown) => unknown) | (string | number)[] | null, space?: string | number): Result<string, JSONStringifyError>;
80
+ /**
81
+ * Safely decodes a URI component. Wraps decodeURIComponent, which throws
82
+ * a URIError on malformed percent-encoded sequences.
83
+ *
84
+ * @param input The string to decode
85
+ * @returns Ok(decoded string) or Err(URIError_)
86
+ */
87
+ declare function tryURIDecode(input: string): Result<string, URIError_>;
88
+ /**
89
+ * Safely encodes a URI component. Wraps encodeURIComponent, which throws
90
+ * a URIError on lone surrogate characters.
91
+ *
92
+ * @param input The string, number, or boolean to encode
93
+ * @returns Ok(encoded string) or Err(URIError_)
94
+ */
95
+ declare function tryURIEncode(input: string | number | boolean): Result<string, URIError_>;
96
+ /**
97
+ * Safely decodes a base64 string. Wraps atob, which throws a DOMException
98
+ * on malformed base64 or non-Latin1 input.
99
+ *
100
+ * @param input The base64 string to decode
101
+ * @returns Ok(decoded string) or Err(Base64Error)
102
+ */
103
+ declare function tryBase64Decode(input: string): Result<string, Base64Error>;
104
+ /**
105
+ * Safely encodes a string to base64. Wraps btoa, which throws a
106
+ * DOMException on characters outside the Latin1 range.
107
+ *
108
+ * @param input The string to encode
109
+ * @returns Ok(base64 string) or Err(Base64Error)
110
+ */
111
+ declare function tryBase64Encode(input: string): Result<string, Base64Error>;
112
+ /**
113
+ * Safely reads a value from localStorage. localStorage.getItem doesn't
114
+ * throw on its own, but this exists so a get→parse pipeline stays
115
+ * consistently Result-shaped throughout (pair with tryJSONParse via chain).
116
+ *
117
+ * @param key The storage key to read
118
+ * @returns Ok(value or null) or Err(StorageError)
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * const result = chain(tryLocalStorageGet('user'), (raw) =>
123
+ * raw === null ? Err({ tag: 'JSONParseError', raw: '' }) : tryJSONParse<User>(raw))
124
+ * ```
125
+ */
126
+ declare function tryLocalStorageGet(key: string): Result<string | null, StorageError>;
127
+ /**
128
+ * Safely writes a value to localStorage. Wraps localStorage.setItem, which
129
+ * throws on quota exceeded or when storage is disabled (e.g. Safari
130
+ * private mode).
131
+ *
132
+ * @param key The storage key to write
133
+ * @param value The string value to store
134
+ * @returns Ok(undefined) or Err(StorageError)
135
+ */
136
+ declare function tryLocalStorageSet(key: string, value: string): Result<void, StorageError>;
137
+ /**
138
+ * Safely constructs a URL. Wraps `new URL(...)`, which throws a TypeError
139
+ * on an invalid URL string.
140
+ *
141
+ * @param input The URL (string or URL) to parse
142
+ * @param base Optional base URL (string or URL) to resolve against
143
+ * @returns Ok(URL) or Err(URLParseError)
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * const result = tryURL(userSuppliedString)
148
+ * if (isOk(result)) {
149
+ * console.log(result.value.hostname)
150
+ * }
151
+ * ```
152
+ */
153
+ declare function tryURL(input: string | URL, base?: string | URL): Result<URL, URLParseError>;
154
+ /**
155
+ * Safely deep-clones a value. Wraps structuredClone, which throws a
156
+ * DataCloneError on non-cloneable values (functions, DOM nodes, etc).
157
+ *
158
+ * @template T The type of the value to clone
159
+ * @param value The value to clone
160
+ * @param options Optional structuredClone options (e.g. a transfer list)
161
+ * @returns Ok(cloned value) or Err(StructuredCloneError)
162
+ */
163
+ declare function tryStructuredClone<T>(value: T, options?: StructuredSerializeOptions): Result<T, StructuredCloneError>;
164
+
165
+ export { tryBase64Decode, tryBase64Encode, tryJSONParse, tryJSONStringify, tryLocalStorageGet, tryLocalStorageSet, tryStructuredClone, tryURIDecode, tryURIEncode, tryURL };
166
+ export type { Base64Error, JSONParseError, JSONStringifyError, StorageError, StructuredCloneError, URIError_, URLParseError };
@@ -0,0 +1,64 @@
1
+ import { safe } from '../safe/index.mjs';
2
+
3
+ function tryJSONParse(raw, reviver) {
4
+ return safe(
5
+ () => JSON.parse(raw, reviver),
6
+ () => ({ tag: "JSONParseError", raw })
7
+ );
8
+ }
9
+ function tryJSONStringify(value, replacer, space) {
10
+ return safe(
11
+ () => JSON.stringify(value, replacer, space),
12
+ () => ({ tag: "JSONStringifyError" })
13
+ );
14
+ }
15
+ function tryURIDecode(input) {
16
+ return safe(
17
+ () => decodeURIComponent(input),
18
+ () => ({ tag: "URIError", input })
19
+ );
20
+ }
21
+ function tryURIEncode(input) {
22
+ return safe(
23
+ () => encodeURIComponent(input),
24
+ () => ({ tag: "URIError", input: String(input) })
25
+ );
26
+ }
27
+ function tryBase64Decode(input) {
28
+ return safe(
29
+ () => atob(input),
30
+ () => ({ tag: "Base64Error", input })
31
+ );
32
+ }
33
+ function tryBase64Encode(input) {
34
+ return safe(
35
+ () => btoa(input),
36
+ () => ({ tag: "Base64Error", input })
37
+ );
38
+ }
39
+ function tryLocalStorageGet(key) {
40
+ return safe(
41
+ () => localStorage.getItem(key),
42
+ () => ({ tag: "StorageError", key })
43
+ );
44
+ }
45
+ function tryLocalStorageSet(key, value) {
46
+ return safe(
47
+ () => localStorage.setItem(key, value),
48
+ () => ({ tag: "StorageError", key })
49
+ );
50
+ }
51
+ function tryURL(input, base) {
52
+ return safe(
53
+ () => new URL(input, base),
54
+ () => ({ tag: "URLParseError", input: String(input) })
55
+ );
56
+ }
57
+ function tryStructuredClone(value, options) {
58
+ return safe(
59
+ () => structuredClone(value, options),
60
+ () => ({ tag: "StructuredCloneError" })
61
+ );
62
+ }
63
+
64
+ export { tryBase64Decode, tryBase64Encode, tryJSONParse, tryJSONStringify, tryLocalStorageGet, tryLocalStorageSet, tryStructuredClone, tryURIDecode, tryURIEncode, tryURL };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@crbroughton/failsafe",
3
+ "type": "module",
4
+ "version": "0.2.0",
5
+ "packageManager": "pnpm@9.15.0",
6
+ "description": "Type-safe error handling utilities for TypeScript",
7
+ "license": "MIT",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/safe/index.d.mts",
12
+ "import": "./dist/safe/index.mjs"
13
+ },
14
+ "./try": {
15
+ "types": "./dist/try/index.d.mts",
16
+ "import": "./dist/try/index.mjs"
17
+ },
18
+ "./pipe": {
19
+ "types": "./dist/pipe/index.d.mts",
20
+ "import": "./dist/pipe/index.mjs"
21
+ }
22
+ },
23
+ "main": "./dist/safe/index.mjs",
24
+ "types": "./dist/safe/index.d.mts",
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "engines": {
29
+ "node": ">=24"
30
+ },
31
+ "scripts": {
32
+ "build": "unbuild",
33
+ "test": "vitest run --coverage",
34
+ "test:watch": "vitest",
35
+ "lint": "eslint .",
36
+ "lint:fix": "eslint . --fix",
37
+ "typecheck": "tsc --noEmit",
38
+ "changeset": "changeset",
39
+ "version": "changeset version",
40
+ "release": "pnpm build && changeset publish"
41
+ },
42
+ "devDependencies": {
43
+ "@antfu/eslint-config": "^3.9.2",
44
+ "@changesets/cli": "^3.0.3",
45
+ "@vitest/coverage-v8": "^2.1.8",
46
+ "eslint": "^9.17.0",
47
+ "happy-dom": "^15.11.7",
48
+ "nx": "^20.3.0",
49
+ "typescript": "^5.7.2",
50
+ "unbuild": "^3.0.1",
51
+ "vitest": "^2.1.8"
52
+ }
53
+ }