@zipbul/result 0.0.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/README.ko.md ADDED
@@ -0,0 +1,383 @@
1
+ # @zipbul/result
2
+
3
+ [English](./README.md) | **한국어**
4
+
5
+ 예외(exception) 없이 에러를 처리하는 경량 Result 타입입니다.
6
+ 클래스로 감싸지 않고 평범한 유니온 값(`T | Err<E>`)을 반환합니다 — 런타임 오버헤드 제로, 완전한 타입 안전성.
7
+
8
+ > throw 없음, try/catch 없음, 래퍼 클래스 없음. 값만 있습니다.
9
+
10
+ <br>
11
+
12
+ ## 📦 설치
13
+
14
+ ```bash
15
+ bun add @zipbul/result
16
+ ```
17
+
18
+ <br>
19
+
20
+ ## 💡 핵심 개념
21
+
22
+ `throw`를 사용하는 전통적 에러 처리는 제어 흐름을 끊고, 타입 정보를 잃으며, 호출자에게 `try/catch` 추측 게임을 강요합니다.
23
+
24
+ ```typescript
25
+ // ❌ Throw — 호출자는 뭐가 올지 전혀 모릅니다
26
+ function parseConfig(raw: string): Config {
27
+ if (!raw) throw new Error('empty input'); // 타입이 뭔가요? 알 수 없음.
28
+ if (!valid(raw)) throw new ValidationError(); // 조용히 상위로 전파됨.
29
+ return JSON.parse(raw);
30
+ }
31
+
32
+ try {
33
+ const config = parseConfig(input);
34
+ } catch (e) {
35
+ // `e`가 뭔가요? Error? ValidationError? JSON.parse의 SyntaxError?
36
+ // TypeScript는 여기서 도와줄 수 없습니다 — `e`는 `unknown`입니다.
37
+ }
38
+ ```
39
+
40
+ ```typescript
41
+ // ✅ Result — 타입 안전, 명시적, 놀라움 없음
42
+ import { err, isErr, type Result } from '@zipbul/result';
43
+
44
+ function parseConfig(raw: string): Result<Config, string> {
45
+ if (!raw) return err('empty input');
46
+ if (!valid(raw)) return err('validation failed');
47
+ return JSON.parse(raw);
48
+ }
49
+
50
+ const result = parseConfig(input);
51
+
52
+ if (isErr(result)) {
53
+ console.error(result.data); // string — TypeScript가 타입을 압니다
54
+ } else {
55
+ console.log(result.host); // Config — 완전히 좁혀짐
56
+ }
57
+ ```
58
+
59
+ <br>
60
+
61
+ ## 🚀 빠른 시작
62
+
63
+ ```typescript
64
+ import { err, isErr, type Result } from '@zipbul/result';
65
+
66
+ interface User {
67
+ id: number;
68
+ name: string;
69
+ }
70
+
71
+ function findUser(id: number): Result<User, string> {
72
+ if (id <= 0) return err('Invalid ID');
73
+
74
+ const user = db.get(id);
75
+ if (!user) return err('User not found');
76
+
77
+ return user;
78
+ }
79
+
80
+ const result = findUser(42);
81
+
82
+ if (isErr(result)) {
83
+ // result는 Err<string>
84
+ console.error(`실패: ${result.data}`);
85
+ } else {
86
+ // result는 User
87
+ console.log(`안녕하세요, ${result.name}`);
88
+ }
89
+ ```
90
+
91
+ <br>
92
+
93
+ ## 📚 API 레퍼런스
94
+
95
+ ### `err()`
96
+
97
+ 불변(immutable) `Err` 값을 생성합니다. 절대 throw하지 않습니다.
98
+
99
+ ```typescript
100
+ import { err } from '@zipbul/result';
101
+ ```
102
+
103
+ | 오버로드 | 반환 | 설명 |
104
+ |:---------|:-----|:-----|
105
+ | `err()` | `Err<never>` | 데이터 없는 에러 |
106
+ | `err<E>(data: E)` | `Err<E>` | 데이터가 첨부된 에러 |
107
+
108
+ ```typescript
109
+ // 데이터 없음 — 단순 신호
110
+ const e1 = err();
111
+ // e1.data → never (접근 불가)
112
+ // e1.stack → 캡처된 스택 트레이스
113
+
114
+ // 데이터 포함 — 에러 상세 정보 전달
115
+ const e2 = err('not found');
116
+ // e2.data → 'not found'
117
+ // e2.stack → 캡처된 스택 트레이스
118
+
119
+ // 풍부한 에러 객체
120
+ const e3 = err({ code: 'TIMEOUT', retryAfter: 3000 });
121
+ // e3.data.code → 'TIMEOUT'
122
+ ```
123
+
124
+ 반환된 `Err`의 프로퍼티:
125
+
126
+ | 프로퍼티 | 타입 | 설명 |
127
+ |:---------|:-----|:-----|
128
+ | `data` | `E` | 첨부된 에러 데이터 |
129
+ | `stack` | `string` | `err()` 호출 지점에서 캡처된 스택 트레이스 |
130
+
131
+ > **불변성** — 모든 `Err`는 `Object.freeze()`됩니다. strict mode에서 프로퍼티를 수정하면 `TypeError`가 발생합니다.
132
+
133
+ <br>
134
+
135
+ ### `isErr()`
136
+
137
+ 값을 `Err<E>`로 좁히는 타입 가드입니다.
138
+
139
+ ```typescript
140
+ import { isErr } from '@zipbul/result';
141
+ ```
142
+
143
+ ```typescript
144
+ function isErr<E = unknown>(value: unknown): value is Err<E>
145
+ ```
146
+
147
+ - `value`가 null이 아닌 객체이고, 마커 프로퍼티가 `true`인 경우에만 `true`를 반환합니다.
148
+ - **절대 throw하지 않습니다** — `null`, `undefined`, 원시값, 예외를 내부적으로 처리합니다.
149
+
150
+ ```typescript
151
+ const result: Result<number, string> = doSomething();
152
+
153
+ if (isErr(result)) {
154
+ // result: Err<string>
155
+ console.error(result.data);
156
+ } else {
157
+ // result: number
158
+ console.log(result + 1);
159
+ }
160
+ ```
161
+
162
+ > **제네릭 `E` 주의사항** — `isErr<E>()`는 타입 단언만 제공합니다. `data`의 형태를 런타임에서 검증하지 않습니다. 호출자가 제네릭이 실제 에러 타입과 일치하는지 보장해야 합니다.
163
+
164
+ <br>
165
+
166
+ ### `Result<T, E>`
167
+
168
+ 평범한 유니온 타입 — 래퍼 클래스가 아닙니다.
169
+
170
+ ```typescript
171
+ type Result<T, E = never> = T | Err<E>;
172
+ ```
173
+
174
+ | 파라미터 | 기본값 | 설명 |
175
+ |:---------|:-------|:-----|
176
+ | `T` | — | 성공 값 타입 |
177
+ | `E` | `never` | 에러 데이터 타입 |
178
+
179
+ ```typescript
180
+ // 단순 — 에러 데이터 없음
181
+ type MayFail = Result<Config>;
182
+
183
+ // 에러 데이터 포함
184
+ type ParseResult = Result<Config, string>;
185
+
186
+ // 풍부한 에러 타입
187
+ type ApiResult = Result<User, { code: string; message: string }>;
188
+ ```
189
+
190
+ <br>
191
+
192
+ ### `Err<E>`
193
+
194
+ `err()`가 반환하는 에러 타입입니다.
195
+
196
+ ```typescript
197
+ type Err<E = never> = {
198
+ stack: string;
199
+ data: E;
200
+ };
201
+ ```
202
+
203
+ > 식별에 사용되는 마커 프로퍼티는 의도적으로 타입에서 제외됩니다. `err()`가 런타임에 내부적으로 추가하고, `isErr()`를 통해서만 판별합니다 — 이렇게 하면 공개 API 표면이 깔끔해지고, 소비자가 구현 세부사항에 의존하는 것을 방지합니다.
204
+
205
+ <br>
206
+
207
+ ### 마커 키(Marker Key)
208
+
209
+ 마커 키는 `Err` 객체를 식별하는 데 사용되는 숨겨진 고유 프로퍼티입니다. 충돌에 강한 문자열이 기본값입니다.
210
+
211
+ ```typescript
212
+ import { DEFAULT_MARKER_KEY, getMarkerKey, setMarkerKey } from '@zipbul/result';
213
+ ```
214
+
215
+ | 내보내기 | 타입 | 설명 |
216
+ |:---------|:-----|:-----|
217
+ | `DEFAULT_MARKER_KEY` | `string` | `'__$$e_9f4a1c7b__'` — 기본 키 |
218
+ | `getMarkerKey()` | `() => string` | 현재 마커 키 반환 |
219
+ | `setMarkerKey(key)` | `(key: string) => void` | 마커 키 변경 |
220
+
221
+ ```typescript
222
+ // 독립 모듈 간 감지 리셋
223
+ import { setMarkerKey, getMarkerKey } from '@zipbul/result';
224
+
225
+ setMarkerKey('__my_app_err__');
226
+ console.log(getMarkerKey()); // '__my_app_err__'
227
+ ```
228
+
229
+ > **검증** — `setMarkerKey()`는 키가 빈 문자열이거나 공백만으로 이루어진 경우 `TypeError`를 던집니다.
230
+ >
231
+ > **주의** — 마커 키를 변경하면 `isErr()`가 이전 키로 생성된 `Err` 객체를 더 이상 인식하지 못합니다. 독립 모듈 간 에러 도메인을 분리해야 할 때만 변경하세요.
232
+
233
+ <br>
234
+
235
+ ## 🔬 고급 사용법
236
+
237
+ ### Result를 반환하는 함수
238
+
239
+ `Result`로 함수 시그니처를 정의하면 에러 경로가 타입 시스템에서 명시적으로 드러납니다.
240
+
241
+ ```typescript
242
+ import { err, isErr, type Result } from '@zipbul/result';
243
+
244
+ interface ValidationError {
245
+ field: string;
246
+ message: string;
247
+ }
248
+
249
+ function validate(input: unknown): Result<ValidData, ValidationError> {
250
+ if (!input || typeof input !== 'object') {
251
+ return err({ field: 'root', message: 'Expected an object' });
252
+ }
253
+ // ... 검증 로직
254
+ return input as ValidData;
255
+ }
256
+
257
+ const result = validate(body);
258
+ if (isErr(result)) {
259
+ return Response.json({ error: result.data }, { status: 400 });
260
+ }
261
+ // result는 여기서 ValidData
262
+ ```
263
+
264
+ ### 결과 체이닝
265
+
266
+ `Result`는 평범한 유니온이므로 `.map()`이나 `.flatMap()`이 없습니다. 표준 제어 흐름을 사용하세요:
267
+
268
+ ```typescript
269
+ function processOrder(orderId: string): Result<Receipt, string> {
270
+ const order = findOrder(orderId);
271
+ if (isErr(order)) return order; // 전파
272
+
273
+ const payment = chargePayment(order);
274
+ if (isErr(payment)) return payment; // 전파
275
+
276
+ return generateReceipt(order, payment);
277
+ }
278
+ ```
279
+
280
+ > 이것은 의도된 설계입니다. `.map()` / `.flatMap()`이 있는 클래스는 런타임 비용을 추가하고 특정 합성 스타일을 강요합니다. 평범한 값 + `isErr()`는 표준 `if`, `switch`, early return 등 원하는 패턴을 자유롭게 사용할 수 있게 합니다.
281
+
282
+ ### 비동기 결과
283
+
284
+ `Promise`와 자연스럽게 작동합니다:
285
+
286
+ ```typescript
287
+ async function fetchUser(id: number): Promise<Result<User, ApiError>> {
288
+ try {
289
+ const res = await fetch(`/api/users/${id}`);
290
+ if (!res.ok) return err({ code: res.status, message: res.statusText });
291
+ return await res.json();
292
+ } catch {
293
+ return err({ code: 0, message: 'Network error' });
294
+ }
295
+ }
296
+ ```
297
+
298
+ ### 스택 트레이스
299
+
300
+ 모든 `Err`는 생성 시점에 스택 트레이스를 캡처하여, `throw` 없이 디버깅이 가능합니다:
301
+
302
+ ```typescript
303
+ const e = err('something went wrong');
304
+ console.log(e.stack);
305
+ // Error
306
+ // at err (/.../err.ts:22:18)
307
+ // at validate (/.../validate.ts:15:12)
308
+ // at handleRequest (/.../server.ts:8:20)
309
+ ```
310
+
311
+ <br>
312
+
313
+ ## 🔌 프레임워크 연동 예시
314
+
315
+ <details>
316
+ <summary><b>Bun.serve</b></summary>
317
+
318
+ ```typescript
319
+ import { err, isErr, type Result } from '@zipbul/result';
320
+
321
+ interface AppError {
322
+ code: string;
323
+ message: string;
324
+ }
325
+
326
+ function parseBody(request: Request): Promise<Result<Payload, AppError>> {
327
+ // ... Result 반환
328
+ }
329
+
330
+ Bun.serve({
331
+ async fetch(request) {
332
+ const body = await parseBody(request);
333
+
334
+ if (isErr(body)) {
335
+ return Response.json(
336
+ { error: body.data.code, message: body.data.message },
337
+ { status: 400 },
338
+ );
339
+ }
340
+
341
+ // body는 Payload
342
+ return Response.json({ ok: true, data: process(body) });
343
+ },
344
+ port: 3000,
345
+ });
346
+ ```
347
+
348
+ </details>
349
+
350
+ <details>
351
+ <summary><b>@zipbul/cors와 함께</b></summary>
352
+
353
+ ```typescript
354
+ import { Cors, CorsAction } from '@zipbul/cors';
355
+ import { isErr } from '@zipbul/result';
356
+
357
+ const corsResult = Cors.create({
358
+ origin: 'https://app.example.com',
359
+ credentials: true,
360
+ });
361
+
362
+ // Cors.create()는 Result<Cors, CorsError>를 반환합니다
363
+ if (isErr(corsResult)) {
364
+ throw new Error(`CORS 설정 에러: ${corsResult.data.message}`);
365
+ }
366
+
367
+ const cors = corsResult;
368
+
369
+ // cors.handle()는 Promise<Result<CorsResult, CorsError>>를 반환합니다
370
+ const result = await cors.handle(request);
371
+
372
+ if (isErr(result)) {
373
+ return new Response('Internal Error', { status: 500 });
374
+ }
375
+ ```
376
+
377
+ </details>
378
+
379
+ <br>
380
+
381
+ ## 📄 라이선스
382
+
383
+ MIT
package/README.md ADDED
@@ -0,0 +1,383 @@
1
+ # @zipbul/result
2
+
3
+ **English** | [한국어](./README.ko.md)
4
+
5
+ A lightweight Result type for error handling without exceptions.
6
+ Returns plain union values (`T | Err<E>`) instead of wrapping in classes — zero runtime overhead, full type safety.
7
+
8
+ > No throw, no try/catch, no wrapper class. Just values.
9
+
10
+ <br>
11
+
12
+ ## 📦 Installation
13
+
14
+ ```bash
15
+ bun add @zipbul/result
16
+ ```
17
+
18
+ <br>
19
+
20
+ ## 💡 Core Concept
21
+
22
+ Traditional error handling with `throw` breaks control flow, loses type information, and forces callers into a `try/catch` guessing game.
23
+
24
+ ```typescript
25
+ // ❌ Throw — caller has no idea what to expect
26
+ function parseConfig(raw: string): Config {
27
+ if (!raw) throw new Error('empty input'); // What type? Unknown.
28
+ if (!valid(raw)) throw new ValidationError(); // Silently propagates up.
29
+ return JSON.parse(raw);
30
+ }
31
+
32
+ try {
33
+ const config = parseConfig(input);
34
+ } catch (e) {
35
+ // What is `e`? Error? ValidationError? SyntaxError from JSON.parse?
36
+ // TypeScript cannot help you here — `e` is `unknown`.
37
+ }
38
+ ```
39
+
40
+ ```typescript
41
+ // ✅ Result — type-safe, explicit, no surprises
42
+ import { err, isErr, type Result } from '@zipbul/result';
43
+
44
+ function parseConfig(raw: string): Result<Config, string> {
45
+ if (!raw) return err('empty input');
46
+ if (!valid(raw)) return err('validation failed');
47
+ return JSON.parse(raw);
48
+ }
49
+
50
+ const result = parseConfig(input);
51
+
52
+ if (isErr(result)) {
53
+ console.error(result.data); // string — TypeScript knows the type
54
+ } else {
55
+ console.log(result.host); // Config — fully narrowed
56
+ }
57
+ ```
58
+
59
+ <br>
60
+
61
+ ## 🚀 Quick Start
62
+
63
+ ```typescript
64
+ import { err, isErr, type Result } from '@zipbul/result';
65
+
66
+ interface User {
67
+ id: number;
68
+ name: string;
69
+ }
70
+
71
+ function findUser(id: number): Result<User, string> {
72
+ if (id <= 0) return err('Invalid ID');
73
+
74
+ const user = db.get(id);
75
+ if (!user) return err('User not found');
76
+
77
+ return user;
78
+ }
79
+
80
+ const result = findUser(42);
81
+
82
+ if (isErr(result)) {
83
+ // result is Err<string>
84
+ console.error(`Failed: ${result.data}`);
85
+ } else {
86
+ // result is User
87
+ console.log(`Hello, ${result.name}`);
88
+ }
89
+ ```
90
+
91
+ <br>
92
+
93
+ ## 📚 API Reference
94
+
95
+ ### `err()`
96
+
97
+ Creates an immutable `Err` value. Never throws.
98
+
99
+ ```typescript
100
+ import { err } from '@zipbul/result';
101
+ ```
102
+
103
+ | Overload | Return | Description |
104
+ |:---------|:-------|:------------|
105
+ | `err()` | `Err<never>` | Error with no data |
106
+ | `err<E>(data: E)` | `Err<E>` | Error with attached data |
107
+
108
+ ```typescript
109
+ // No data — simple signal
110
+ const e1 = err();
111
+ // e1.data → never (cannot access)
112
+ // e1.stack → captured stack trace
113
+
114
+ // With data — carry error details
115
+ const e2 = err('not found');
116
+ // e2.data → 'not found'
117
+ // e2.stack → captured stack trace
118
+
119
+ // Rich error objects
120
+ const e3 = err({ code: 'TIMEOUT', retryAfter: 3000 });
121
+ // e3.data.code → 'TIMEOUT'
122
+ ```
123
+
124
+ Properties of the returned `Err`:
125
+
126
+ | Property | Type | Description |
127
+ |:---------|:-----|:------------|
128
+ | `data` | `E` | The attached error data |
129
+ | `stack` | `string` | Stack trace captured at `err()` call site |
130
+
131
+ > **Immutability** — every `Err` is `Object.freeze()`d. Attempting to modify properties in strict mode throws a `TypeError`.
132
+
133
+ <br>
134
+
135
+ ### `isErr()`
136
+
137
+ Type guard that narrows a value to `Err<E>`.
138
+
139
+ ```typescript
140
+ import { isErr } from '@zipbul/result';
141
+ ```
142
+
143
+ ```typescript
144
+ function isErr<E = unknown>(value: unknown): value is Err<E>
145
+ ```
146
+
147
+ - Returns `true` if `value` is a non-null object with the marker property set to `true`.
148
+ - **Never throws** — handles `null`, `undefined`, primitives, and exceptions internally.
149
+
150
+ ```typescript
151
+ const result: Result<number, string> = doSomething();
152
+
153
+ if (isErr(result)) {
154
+ // result: Err<string>
155
+ console.error(result.data);
156
+ } else {
157
+ // result: number
158
+ console.log(result + 1);
159
+ }
160
+ ```
161
+
162
+ > **Generic `E` caveat** — `isErr<E>()` provides a type assertion only. It does not validate the shape of `data` at runtime. Callers must ensure the generic matches the actual error type.
163
+
164
+ <br>
165
+
166
+ ### `Result<T, E>`
167
+
168
+ A plain union type — not a wrapper class.
169
+
170
+ ```typescript
171
+ type Result<T, E = never> = T | Err<E>;
172
+ ```
173
+
174
+ | Parameter | Default | Description |
175
+ |:----------|:--------|:------------|
176
+ | `T` | — | Success value type |
177
+ | `E` | `never` | Error data type |
178
+
179
+ ```typescript
180
+ // Simple — no error data
181
+ type MayFail = Result<Config>;
182
+
183
+ // With error data
184
+ type ParseResult = Result<Config, string>;
185
+
186
+ // Rich error types
187
+ type ApiResult = Result<User, { code: string; message: string }>;
188
+ ```
189
+
190
+ <br>
191
+
192
+ ### `Err<E>`
193
+
194
+ The error type returned by `err()`.
195
+
196
+ ```typescript
197
+ type Err<E = never> = {
198
+ stack: string;
199
+ data: E;
200
+ };
201
+ ```
202
+
203
+ > The marker property used for identification is deliberately excluded from the type. It is added internally by `err()` and checked by `isErr()` — this keeps the public API surface clean and prevents consumers from depending on implementation details.
204
+
205
+ <br>
206
+
207
+ ### Marker Key
208
+
209
+ The marker key is a unique hidden property used to identify `Err` objects. It defaults to a collision-resistant string.
210
+
211
+ ```typescript
212
+ import { DEFAULT_MARKER_KEY, getMarkerKey, setMarkerKey } from '@zipbul/result';
213
+ ```
214
+
215
+ | Export | Type | Description |
216
+ |:-------|:-----|:------------|
217
+ | `DEFAULT_MARKER_KEY` | `string` | `'__$$e_9f4a1c7b__'` — the default key |
218
+ | `getMarkerKey()` | `() => string` | Returns the current marker key |
219
+ | `setMarkerKey(key)` | `(key: string) => void` | Changes the marker key |
220
+
221
+ ```typescript
222
+ // Reset detection across independent modules
223
+ import { setMarkerKey, getMarkerKey } from '@zipbul/result';
224
+
225
+ setMarkerKey('__my_app_err__');
226
+ console.log(getMarkerKey()); // '__my_app_err__'
227
+ ```
228
+
229
+ > **Validation** — `setMarkerKey()` throws `TypeError` if the key is empty or whitespace-only.
230
+ >
231
+ > **Warning** — changing the marker key means `isErr()` will no longer recognize `Err` objects created with the previous key. Only change this if you need to isolate error domains across independent modules.
232
+
233
+ <br>
234
+
235
+ ## 🔬 Advanced Usage
236
+
237
+ ### Result-returning functions
238
+
239
+ Define function signatures with `Result` to make error paths explicit in the type system.
240
+
241
+ ```typescript
242
+ import { err, isErr, type Result } from '@zipbul/result';
243
+
244
+ interface ValidationError {
245
+ field: string;
246
+ message: string;
247
+ }
248
+
249
+ function validate(input: unknown): Result<ValidData, ValidationError> {
250
+ if (!input || typeof input !== 'object') {
251
+ return err({ field: 'root', message: 'Expected an object' });
252
+ }
253
+ // ... validation logic
254
+ return input as ValidData;
255
+ }
256
+
257
+ const result = validate(body);
258
+ if (isErr(result)) {
259
+ return Response.json({ error: result.data }, { status: 400 });
260
+ }
261
+ // result is ValidData here
262
+ ```
263
+
264
+ ### Chaining results
265
+
266
+ Since `Result` is a plain union, there's no `.map()` or `.flatMap()`. Use standard control flow:
267
+
268
+ ```typescript
269
+ function processOrder(orderId: string): Result<Receipt, string> {
270
+ const order = findOrder(orderId);
271
+ if (isErr(order)) return order; // propagate
272
+
273
+ const payment = chargePayment(order);
274
+ if (isErr(payment)) return payment; // propagate
275
+
276
+ return generateReceipt(order, payment);
277
+ }
278
+ ```
279
+
280
+ > This is intentional. Classes with `.map()` / `.flatMap()` add runtime cost and force a specific composition style. Plain values + `isErr()` let you use standard `if`, `switch`, early return, and any other pattern you prefer.
281
+
282
+ ### Async results
283
+
284
+ Works naturally with `Promise`:
285
+
286
+ ```typescript
287
+ async function fetchUser(id: number): Promise<Result<User, ApiError>> {
288
+ try {
289
+ const res = await fetch(`/api/users/${id}`);
290
+ if (!res.ok) return err({ code: res.status, message: res.statusText });
291
+ return await res.json();
292
+ } catch {
293
+ return err({ code: 0, message: 'Network error' });
294
+ }
295
+ }
296
+ ```
297
+
298
+ ### Stack traces
299
+
300
+ Every `Err` captures a stack trace at creation time, enabling debugging without `throw`:
301
+
302
+ ```typescript
303
+ const e = err('something went wrong');
304
+ console.log(e.stack);
305
+ // Error
306
+ // at err (/.../err.ts:22:18)
307
+ // at validate (/.../validate.ts:15:12)
308
+ // at handleRequest (/.../server.ts:8:20)
309
+ ```
310
+
311
+ <br>
312
+
313
+ ## 🔌 Framework Integration Examples
314
+
315
+ <details>
316
+ <summary><b>Bun.serve</b></summary>
317
+
318
+ ```typescript
319
+ import { err, isErr, type Result } from '@zipbul/result';
320
+
321
+ interface AppError {
322
+ code: string;
323
+ message: string;
324
+ }
325
+
326
+ function parseBody(request: Request): Promise<Result<Payload, AppError>> {
327
+ // ... returns Result
328
+ }
329
+
330
+ Bun.serve({
331
+ async fetch(request) {
332
+ const body = await parseBody(request);
333
+
334
+ if (isErr(body)) {
335
+ return Response.json(
336
+ { error: body.data.code, message: body.data.message },
337
+ { status: 400 },
338
+ );
339
+ }
340
+
341
+ // body is Payload
342
+ return Response.json({ ok: true, data: process(body) });
343
+ },
344
+ port: 3000,
345
+ });
346
+ ```
347
+
348
+ </details>
349
+
350
+ <details>
351
+ <summary><b>With @zipbul/cors</b></summary>
352
+
353
+ ```typescript
354
+ import { Cors, CorsAction } from '@zipbul/cors';
355
+ import { isErr } from '@zipbul/result';
356
+
357
+ const corsResult = Cors.create({
358
+ origin: 'https://app.example.com',
359
+ credentials: true,
360
+ });
361
+
362
+ // Cors.create() returns Result<Cors, CorsError>
363
+ if (isErr(corsResult)) {
364
+ throw new Error(`CORS config error: ${corsResult.data.message}`);
365
+ }
366
+
367
+ const cors = corsResult;
368
+
369
+ // cors.handle() returns Promise<Result<CorsResult, CorsError>>
370
+ const result = await cors.handle(request);
371
+
372
+ if (isErr(result)) {
373
+ return new Response('Internal Error', { status: 500 });
374
+ }
375
+ ```
376
+
377
+ </details>
378
+
379
+ <br>
380
+
381
+ ## 📄 License
382
+
383
+ MIT
@@ -0,0 +1,4 @@
1
+ export { err } from './src/err';
2
+ export { isErr } from './src/is-err';
3
+ export { DEFAULT_MARKER_KEY, getMarkerKey, setMarkerKey } from './src/constants';
4
+ export type { Err, Result } from './src/types';
package/dist/index.js ADDED
@@ -0,0 +1,47 @@
1
+ // @bun
2
+ // src/constants.ts
3
+ var DEFAULT_MARKER_KEY = "__$$e_9f4a1c7b__";
4
+ var currentMarkerKey = DEFAULT_MARKER_KEY;
5
+ function getMarkerKey() {
6
+ return currentMarkerKey;
7
+ }
8
+ function setMarkerKey(key) {
9
+ if (key.trim().length === 0) {
10
+ throw new TypeError("Marker key must be a non-empty string");
11
+ }
12
+ currentMarkerKey = key;
13
+ }
14
+
15
+ // src/err.ts
16
+ function err(data) {
17
+ let stack;
18
+ try {
19
+ stack = new Error().stack ?? "";
20
+ } catch {
21
+ stack = "";
22
+ }
23
+ const result = {
24
+ [getMarkerKey()]: true,
25
+ stack,
26
+ data
27
+ };
28
+ return Object.freeze(result);
29
+ }
30
+ // src/is-err.ts
31
+ function isErr(value) {
32
+ try {
33
+ return value !== null && value !== undefined && typeof value === "object" && value[getMarkerKey()] === true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+ export {
39
+ setMarkerKey,
40
+ isErr,
41
+ getMarkerKey,
42
+ err,
43
+ DEFAULT_MARKER_KEY
44
+ };
45
+
46
+ //# debugId=5E29B525CDB4D96664756E2164756E21
47
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/constants.ts", "../src/err.ts", "../src/is-err.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * 기본 에러 마커 키.\n * 에러 객체의 판별 프로퍼티명으로 사용된다.\n * zipbul 프레임워크와 무관한 유니크한 값으로, 충돌 가능성을 최소화한다.\n */\nexport const DEFAULT_MARKER_KEY = '__$$e_9f4a1c7b__';\n\nlet currentMarkerKey: string = DEFAULT_MARKER_KEY;\n\n/**\n * 현재 설정된 마커 키를 반환한다.\n *\n * @returns 현재 마커 키 문자열\n */\nexport function getMarkerKey(): string {\n return currentMarkerKey;\n}\n\n/**\n * 마커 키를 변경한다.\n * error()와 isError()가 이 키를 참조하여 에러를 판별한다.\n * 빈 문자열 및 공백만으로 이루어진 문자열은 허용하지 않는다.\n *\n * @param key - 새 마커 키. 비어있지 않은(공백 제외) 문자열이어야 한다.\n * @throws {TypeError} key가 빈/공백 문자열인 경우\n */\nexport function setMarkerKey(key: string): void {\n if (key.trim().length === 0) {\n throw new TypeError('Marker key must be a non-empty string');\n }\n currentMarkerKey = key;\n}\n",
6
+ "import type { Err } from './types';\nimport { getMarkerKey } from './constants';\n\n/**\n * Err를 생성한다.\n * 절대 throw하지 않는다.\n * 반환 전 Object.freeze를 적용한다.\n *\n * @returns 동결(frozen)된 Err (data 없음)\n */\nexport function err(): Err;\n/**\n * @param data - 에러에 첨부할 데이터. 타입 무관.\n * @returns 동결(frozen)된 Err<E>\n */\nexport function err<E>(data: E): Err<E>;\nexport function err<E = never>(data?: E): Err<E> {\n let stack: string;\n try {\n stack = new Error().stack ?? '';\n } catch {\n stack = '';\n }\n\n const result = {\n [getMarkerKey()]: true,\n stack,\n data: data as E,\n };\n\n return Object.freeze(result) as Err<E>;\n}\n",
7
+ "import type { Err } from './types';\nimport { getMarkerKey } from './constants';\n\n/**\n * 값이 Err인지 판별하는 타입 가드.\n * 현재 설정된 마커 키(`getMarkerKey()`)에 해당하는 프로퍼티가\n * `=== true`인 경우에만 true를 반환한다.\n * 절대 throw하지 않는다.\n *\n * 주의: 제네릭 E는 런타임 검증 없이 타입 단언만 제공한다.\n * data의 구조는 호출자가 보장해야 한다.\n *\n * @param value - 판별 대상. 타입 무관.\n * @returns value가 Err이면 true\n */\nexport function isErr<E = unknown>(\n value: unknown,\n): value is Err<E> {\n try {\n return (\n value !== null &&\n value !== undefined &&\n typeof value === 'object' &&\n (value as Record<string, unknown>)[getMarkerKey()] === true\n );\n } catch {\n return false;\n }\n}\n"
8
+ ],
9
+ "mappings": ";;AAKO,IAAM,qBAAqB;AAElC,IAAI,mBAA2B;AAOxB,SAAS,YAAY,GAAW;AAAA,EACrC,OAAO;AAAA;AAWF,SAAS,YAAY,CAAC,KAAmB;AAAA,EAC9C,IAAI,IAAI,KAAK,EAAE,WAAW,GAAG;AAAA,IAC3B,MAAM,IAAI,UAAU,uCAAuC;AAAA,EAC7D;AAAA,EACA,mBAAmB;AAAA;;;ACdd,SAAS,GAAc,CAAC,MAAkB;AAAA,EAC/C,IAAI;AAAA,EACJ,IAAI;AAAA,IACF,QAAQ,IAAI,MAAM,EAAE,SAAS;AAAA,IAC7B,MAAM;AAAA,IACN,QAAQ;AAAA;AAAA,EAGV,MAAM,SAAS;AAAA,KACZ,aAAa,IAAI;AAAA,IAClB;AAAA,IACA;AAAA,EACF;AAAA,EAEA,OAAO,OAAO,OAAO,MAAM;AAAA;;ACftB,SAAS,KAAkB,CAChC,OACiB;AAAA,EACjB,IAAI;AAAA,IACF,OACE,UAAU,QACV,UAAU,aACV,OAAO,UAAU,YAChB,MAAkC,aAAa,OAAO;AAAA,IAEzD,MAAM;AAAA,IACN,OAAO;AAAA;AAAA;",
10
+ "debugId": "5E29B525CDB4D96664756E2164756E21",
11
+ "names": []
12
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 기본 에러 마커 키.
3
+ * 에러 객체의 판별 프로퍼티명으로 사용된다.
4
+ * zipbul 프레임워크와 무관한 유니크한 값으로, 충돌 가능성을 최소화한다.
5
+ */
6
+ export declare const DEFAULT_MARKER_KEY = "__$$e_9f4a1c7b__";
7
+ /**
8
+ * 현재 설정된 마커 키를 반환한다.
9
+ *
10
+ * @returns 현재 마커 키 문자열
11
+ */
12
+ export declare function getMarkerKey(): string;
13
+ /**
14
+ * 마커 키를 변경한다.
15
+ * error()와 isError()가 이 키를 참조하여 에러를 판별한다.
16
+ * 빈 문자열 및 공백만으로 이루어진 문자열은 허용하지 않는다.
17
+ *
18
+ * @param key - 새 마커 키. 비어있지 않은(공백 제외) 문자열이어야 한다.
19
+ * @throws {TypeError} key가 빈/공백 문자열인 경우
20
+ */
21
+ export declare function setMarkerKey(key: string): void;
@@ -0,0 +1,14 @@
1
+ import type { Err } from './types';
2
+ /**
3
+ * Err를 생성한다.
4
+ * 절대 throw하지 않는다.
5
+ * 반환 전 Object.freeze를 적용한다.
6
+ *
7
+ * @returns 동결(frozen)된 Err (data 없음)
8
+ */
9
+ export declare function err(): Err;
10
+ /**
11
+ * @param data - 에러에 첨부할 데이터. 타입 무관.
12
+ * @returns 동결(frozen)된 Err<E>
13
+ */
14
+ export declare function err<E>(data: E): Err<E>;
@@ -0,0 +1,14 @@
1
+ import type { Err } from './types';
2
+ /**
3
+ * 값이 Err인지 판별하는 타입 가드.
4
+ * 현재 설정된 마커 키(`getMarkerKey()`)에 해당하는 프로퍼티가
5
+ * `=== true`인 경우에만 true를 반환한다.
6
+ * 절대 throw하지 않는다.
7
+ *
8
+ * 주의: 제네릭 E는 런타임 검증 없이 타입 단언만 제공한다.
9
+ * data의 구조는 호출자가 보장해야 한다.
10
+ *
11
+ * @param value - 판별 대상. 타입 무관.
12
+ * @returns value가 Err이면 true
13
+ */
14
+ export declare function isErr<E = unknown>(value: unknown): value is Err<E>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 에러 타입.
3
+ * 마커 프로퍼티는 타입에 포함하지 않는다.
4
+ * err() 함수가 런타임에 마커를 동적 추가하며,
5
+ * isErr() 타입 가드를 통해서만 판별한다.
6
+ *
7
+ * @template E - 에러에 첨부할 추가 데이터의 타입. 기본값은 never.
8
+ */
9
+ export type Err<E = never> = {
10
+ stack: string;
11
+ data: E;
12
+ };
13
+ /**
14
+ * 성공(T) 또는 에러(Err<E>)를 표현하는 유니온 타입.
15
+ * wrapper 클래스가 아닌 plain union으로, 런타임 오버헤드가 없다.
16
+ *
17
+ * @template T - 성공값 타입
18
+ * @template E - 에러 데이터 타입. 기본값은 never.
19
+ */
20
+ export type Result<T, E = never> = T | Err<E>;
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@zipbul/result",
3
+ "version": "0.0.1",
4
+ "description": "Lightweight Result type for error handling without exceptions",
5
+ "license": "MIT",
6
+ "author": "Junhyung Park (https://github.com/parkrevil)",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/zipbul/toolkit",
10
+ "directory": "packages/result"
11
+ },
12
+ "bugs": "https://github.com/zipbul/toolkit/issues",
13
+ "homepage": "https://github.com/zipbul/toolkit/tree/main/packages/result#readme",
14
+ "keywords": ["result", "error", "either", "typescript", "zipbul"],
15
+ "engines": {
16
+ "bun": ">=1.0.0"
17
+ },
18
+ "type": "module",
19
+ "module": "dist/index.js",
20
+ "types": "dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ }
26
+ },
27
+ "files": ["dist"],
28
+ "scripts": {
29
+ "build": "bun build index.ts --outdir dist --target bun --format esm --sourcemap=linked && tsc -p tsconfig.build.json",
30
+ "test": "bun test",
31
+ "coverage": "bun test --coverage"
32
+ }
33
+ }