hanzio 2.1.0 → 3.0.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/LICENSE +21 -0
- package/README.md +24 -10
- package/dist/api-wrapper/endpoint.d.ts +8 -5
- package/dist/api-wrapper/transport.d.ts +1 -0
- package/dist/api-wrapper/types.d.ts +7 -0
- package/dist/{index-j1ybktbr.js → index-zbj9s3s3.js} +6 -4
- package/dist/src/api-wrapper/index.js +1 -1
- package/dist/src/index.js +8 -2
- package/dist/src/secrets/index.js +1 -1
- package/dist/typedswitch/index.d.ts +55 -34
- package/package.json +11 -3
- package/src/api-wrapper/README.md +741 -0
- package/src/api-wrapper/SKILL.md +256 -0
- package/src/api-wrapper/create-client.ts +9 -3
- package/src/api-wrapper/endpoint.ts +19 -5
- package/src/api-wrapper/transport.ts +6 -2
- package/src/api-wrapper/types.ts +13 -0
- package/src/crypto/README.md +144 -0
- package/src/format/README.md +120 -0
- package/src/i18n/README.md +65 -0
- package/src/jwt/README.md +70 -0
- package/src/p-queue/README.md +237 -0
- package/src/secrets/README.md +413 -0
- package/src/sitemap/README.md +61 -0
- package/src/storage/README.md +71 -0
- package/src/typedswitch/README.md +335 -0
- package/src/typedswitch/index.ts +97 -83
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# typedswitch
|
|
2
|
+
|
|
3
|
+
A type-safe, exhaustive switch expression for TypeScript. Handle discriminated unions and string literals with full type inference, compile-time exhaustiveness checking, and async support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install typedswitch
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add typedswitch
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add typedswitch
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Full type inference** — Return types are automatically inferred from your handlers
|
|
22
|
+
- **Exhaustiveness checking** — TypeScript ensures all cases are handled at compile time
|
|
23
|
+
- **Discriminated union support** — Works with any discriminant key, not just `type`
|
|
24
|
+
- **Async-aware** — Async handlers preserve their `Promise` return type
|
|
25
|
+
- **Grouped cases** — Share one handler between several values, fully typed
|
|
26
|
+
- **Default handlers** — Handle remaining cases with a fallback
|
|
27
|
+
- **Typo-proof cases** — A case key that isn't a value of the union is a compile error
|
|
28
|
+
- **`assertNever`** — Exhaustiveness checking for regular `switch` statements
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Plain Value Input
|
|
33
|
+
|
|
34
|
+
Switch on a string, number, boolean or enum value directly:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { typedSwitch } from 'typedswitch'
|
|
38
|
+
|
|
39
|
+
type Status = 'success' | 'error' | 'pending'
|
|
40
|
+
|
|
41
|
+
const status: Status = 'success'
|
|
42
|
+
|
|
43
|
+
// All cases required — TypeScript enforces exhaustiveness
|
|
44
|
+
const message = typedSwitch(status, {
|
|
45
|
+
success: () => 'Operation completed!',
|
|
46
|
+
error: () => 'Something went wrong',
|
|
47
|
+
pending: () => 'Please wait...',
|
|
48
|
+
})
|
|
49
|
+
// message: string
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Each handler receives the narrowed value:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
const result = typedSwitch(status, {
|
|
56
|
+
success: (val) => val.toUpperCase(), // val: 'success'
|
|
57
|
+
error: (val) => val.toUpperCase(), // val: 'error'
|
|
58
|
+
pending: (val) => val.toUpperCase(), // val: 'pending'
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Numbers and booleans are keyed by `String(value)`, and each handler still
|
|
63
|
+
receives the typed value:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
typedSwitch(code, {
|
|
67
|
+
200: (c) => 'ok', // c: 200
|
|
68
|
+
404: (c) => `missing ${c}`, // c: 404
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
typedSwitch(enabled, {
|
|
72
|
+
true: () => 'on',
|
|
73
|
+
false: () => 'off',
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Discriminated Union Input
|
|
78
|
+
|
|
79
|
+
Switch on objects with a string, number, or boolean discriminant property (like `type`, `kind`, `status`, etc.):
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
type Event =
|
|
83
|
+
| { type: 'click'; x: number; y: number }
|
|
84
|
+
| { type: 'scroll'; offset: number }
|
|
85
|
+
| { type: 'keypress'; key: string }
|
|
86
|
+
|
|
87
|
+
const event: Event = { type: 'click', x: 100, y: 200 }
|
|
88
|
+
|
|
89
|
+
// Specify the discriminant key as the second argument
|
|
90
|
+
const description = typedSwitch(event, 'type', {
|
|
91
|
+
click: (e) => `Clicked at (${e.x}, ${e.y})`, // e: { type: 'click'; x: number; y: number }
|
|
92
|
+
scroll: (e) => `Scrolled ${e.offset}px`, // e: { type: 'scroll'; offset: number }
|
|
93
|
+
keypress: (e) => `Pressed ${e.key}`, // e: { type: 'keypress'; key: string }
|
|
94
|
+
})
|
|
95
|
+
// description: string
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Works with any discriminant key:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
type Order =
|
|
102
|
+
| { status: 'pending'; createdAt: Date }
|
|
103
|
+
| { status: 'completed'; completedAt: Date }
|
|
104
|
+
| { status: 'cancelled'; reason: string }
|
|
105
|
+
|
|
106
|
+
const order: Order = { status: 'completed', completedAt: new Date() }
|
|
107
|
+
|
|
108
|
+
const info = typedSwitch(order, 'status', {
|
|
109
|
+
pending: (o) => `Created: ${o.createdAt}`,
|
|
110
|
+
completed: (o) => `Done: ${o.completedAt}`,
|
|
111
|
+
cancelled: (o) => `Cancelled: ${o.reason}`,
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Number and boolean discriminants work too. Cases are keyed by `String(value)`:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
type Response =
|
|
119
|
+
| { code: 200; body: string }
|
|
120
|
+
| { code: 404; path: string }
|
|
121
|
+
|
|
122
|
+
typedSwitch(response, 'code', {
|
|
123
|
+
200: (r) => r.body, // r: { code: 200; body: string }
|
|
124
|
+
404: (r) => `Missing ${r.path}`, // r: { code: 404; path: string }
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
type Result = { ok: true; value: number } | { ok: false; error: string }
|
|
128
|
+
|
|
129
|
+
typedSwitch(result, 'ok', {
|
|
130
|
+
true: (r) => r.value,
|
|
131
|
+
false: (r) => r.error,
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Grouped Cases
|
|
136
|
+
|
|
137
|
+
Spread `typedSwitchGroup` into the cases to share one handler between several
|
|
138
|
+
values. Pass the switched value (and key) so the types are inferred. The
|
|
139
|
+
handler receives the union of the grouped members, and exhaustiveness checking,
|
|
140
|
+
default handlers and return type inference keep working:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { typedSwitch, typedSwitchGroup } from 'typedswitch'
|
|
144
|
+
|
|
145
|
+
const description = typedSwitch(event, 'type', {
|
|
146
|
+
...typedSwitchGroup(event, 'type', ['click', 'scroll'], (e) => e.type), // e: click or scroll event
|
|
147
|
+
keypress: (e) => `Pressed ${e.key}`,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const message = typedSwitch(status, {
|
|
151
|
+
...typedSwitchGroup(status, ['error', 'cancelled'], (s) => `Stopped: ${s}`), // s: 'error' | 'cancelled'
|
|
152
|
+
success: () => 'Done!',
|
|
153
|
+
pending: () => 'Please wait...',
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Group keys must be values of the discriminant. A case written after the spread
|
|
158
|
+
replaces the grouped handler for that key; writing it before the spread is a
|
|
159
|
+
type error. If two groups list the same key, the later group wins.
|
|
160
|
+
|
|
161
|
+
### Default Handler
|
|
162
|
+
|
|
163
|
+
Handle a subset of cases and catch the rest with a default handler:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
type Status = 'success' | 'error' | 'pending' | 'cancelled'
|
|
167
|
+
|
|
168
|
+
const status: Status = 'cancelled'
|
|
169
|
+
|
|
170
|
+
// Only handle specific cases, default handles the rest
|
|
171
|
+
const message = typedSwitch(
|
|
172
|
+
status,
|
|
173
|
+
{
|
|
174
|
+
success: () => 'Done!',
|
|
175
|
+
error: () => 'Failed!',
|
|
176
|
+
},
|
|
177
|
+
(val) => `Unhandled status: ${val}` // val: 'pending' | 'cancelled' (unhandled cases only)
|
|
178
|
+
)
|
|
179
|
+
// message: string
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Only required, defined handlers narrow the default handler's input. Optional
|
|
183
|
+
handlers (including reusable `PartialStringSwitchCases` and
|
|
184
|
+
`PartialObjectSwitchCases` maps) can be absent, so their cases remain in the
|
|
185
|
+
default handler's input type.
|
|
186
|
+
|
|
187
|
+
Works with discriminated unions too:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const description = typedSwitch(
|
|
191
|
+
event,
|
|
192
|
+
'type',
|
|
193
|
+
{
|
|
194
|
+
click: (e) => `Clicked at (${e.x}, ${e.y})`,
|
|
195
|
+
},
|
|
196
|
+
(e) => `Unhandled event: ${e.type}` // e: scroll or keypress event (unhandled members only)
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Async Handlers
|
|
201
|
+
|
|
202
|
+
Mix sync and async handlers freely. `typedSwitch` returns the selected handler's value directly, so async branches preserve their `Promise` return type while sync branches remain sync:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// All sync — returns string
|
|
206
|
+
const syncResult = typedSwitch(status, {
|
|
207
|
+
success: () => 'ok',
|
|
208
|
+
error: () => 'err',
|
|
209
|
+
pending: () => 'wait',
|
|
210
|
+
})
|
|
211
|
+
// syncResult: string
|
|
212
|
+
|
|
213
|
+
// All async — returns Promise<string>
|
|
214
|
+
const asyncResult = typedSwitch(status, {
|
|
215
|
+
success: async () => {
|
|
216
|
+
const data = await fetchData()
|
|
217
|
+
return data.message
|
|
218
|
+
},
|
|
219
|
+
error: async () => 'err',
|
|
220
|
+
pending: async () => 'wait',
|
|
221
|
+
})
|
|
222
|
+
// asyncResult: Promise<string>
|
|
223
|
+
|
|
224
|
+
const message = await asyncResult
|
|
225
|
+
|
|
226
|
+
// Mixed sync/async — returns string | Promise<string>
|
|
227
|
+
const mixedResult = typedSwitch(status, {
|
|
228
|
+
success: () => 'ok',
|
|
229
|
+
error: async () => 'err',
|
|
230
|
+
pending: () => 'wait',
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Return Type Inference
|
|
235
|
+
|
|
236
|
+
Return types are inferred from all handlers as a union:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
const result = typedSwitch(status, {
|
|
240
|
+
success: () => ({ ok: true, data: 'hello' }),
|
|
241
|
+
error: () => ({ ok: false, code: 500 }),
|
|
242
|
+
pending: () => null,
|
|
243
|
+
})
|
|
244
|
+
// result: { ok: true; data: string } | { ok: false; code: number } | null
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Enforcing a Return Type
|
|
248
|
+
|
|
249
|
+
Annotate the result. Every handler's return type must fit, and exhaustiveness
|
|
250
|
+
checking still applies:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
interface HasId {
|
|
254
|
+
id: string
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const result: HasId = typedSwitch(status, {
|
|
258
|
+
success: () => ({ id: '123', name: 'John' }),
|
|
259
|
+
error: () => ({ id: '456', code: 500 }),
|
|
260
|
+
pending: () => ({ id: '789' }),
|
|
261
|
+
})
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Error Handling
|
|
265
|
+
|
|
266
|
+
When a case is unhandled at runtime (e.g., due to unsafe casts), `typedSwitch` throws a descriptive error:
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
const status = 'unknown' as Status
|
|
270
|
+
|
|
271
|
+
typedSwitch(status, {
|
|
272
|
+
success: () => 'ok',
|
|
273
|
+
error: () => 'err',
|
|
274
|
+
})
|
|
275
|
+
// Throws: "Unhandled case: unknown. Available cases: success, error"
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
For discriminated unions:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// Throws: "Unhandled case: unknown (discriminant key: "type"). Available cases: click, scroll, keypress"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Object discriminants must be strings, numbers, or booleans. Missing or other discriminant values throw a runtime error if they reach `typedSwitch`.
|
|
285
|
+
|
|
286
|
+
## `assertNever`
|
|
287
|
+
|
|
288
|
+
For regular `switch` statements, put `assertNever` in the `default` branch. It fails to compile if a case is missing, and throws at runtime if an unexpected value gets through:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import { assertNever } from 'typedswitch'
|
|
292
|
+
|
|
293
|
+
function label(status: Status) {
|
|
294
|
+
switch (status) {
|
|
295
|
+
case 'success': return 'Done'
|
|
296
|
+
case 'error': return 'Failed'
|
|
297
|
+
case 'pending': return 'Waiting'
|
|
298
|
+
default: return assertNever(status) // compile error if a case is missing
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## API Reference
|
|
304
|
+
|
|
305
|
+
### `typedSwitch(value, cases)`
|
|
306
|
+
|
|
307
|
+
Switch on a string, number or boolean value with exhaustive case handling.
|
|
308
|
+
|
|
309
|
+
### `typedSwitch(value, cases, defaultHandler)`
|
|
310
|
+
|
|
311
|
+
Switch on a string, number or boolean value with partial cases and a default fallback.
|
|
312
|
+
|
|
313
|
+
### `typedSwitch(object, key, cases)`
|
|
314
|
+
|
|
315
|
+
Switch on a discriminated union using the specified discriminant key (string, number, or boolean values).
|
|
316
|
+
|
|
317
|
+
### `typedSwitch(object, key, cases, defaultHandler)`
|
|
318
|
+
|
|
319
|
+
Switch on a discriminated union with partial cases and a default fallback.
|
|
320
|
+
|
|
321
|
+
### `typedSwitchGroup(value, keys, handler)` / `typedSwitchGroup(object, key, keys, handler)`
|
|
322
|
+
|
|
323
|
+
Returns cases that all call `handler`, for spreading into `typedSwitch` cases.
|
|
324
|
+
|
|
325
|
+
### `assertNever(value, message?)`
|
|
326
|
+
|
|
327
|
+
Takes a `never` value and always throws (`Unexpected value: ${value}` or `message`). Use it for exhaustive `switch` statements.
|
|
328
|
+
|
|
329
|
+
### Exported Helper Types
|
|
330
|
+
|
|
331
|
+
`StringSwitchCases`, `PartialStringSwitchCases`, `ObjectSwitchCases`, and `PartialObjectSwitchCases` are exported for users who want to annotate reusable case maps.
|
|
332
|
+
|
|
333
|
+
## License
|
|
334
|
+
|
|
335
|
+
MIT
|
package/src/typedswitch/index.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Supports two input modes:
|
|
5
5
|
*
|
|
6
|
-
* 1.
|
|
6
|
+
* 1. PLAIN VALUE INPUT - switch on a string, number or boolean value directly.
|
|
7
|
+
* Cases are keyed by `String(value)`:
|
|
7
8
|
*
|
|
8
9
|
* @example
|
|
9
10
|
* const status: 'success' | 'error' = 'success'
|
|
@@ -19,6 +20,9 @@
|
|
|
19
20
|
* success: (val) => 'handled success',
|
|
20
21
|
* }, (val) => 'default handler')
|
|
21
22
|
*
|
|
23
|
+
* typedSwitch(code, { 200: (c) => 'ok', 404: (c) => 'missing' }) // c: 200, 404
|
|
24
|
+
* typedSwitch(flag, { true: () => 'on', false: () => 'off' })
|
|
25
|
+
*
|
|
22
26
|
*
|
|
23
27
|
* 2. OBJECT INPUT - switch on a discriminated union with custom key:
|
|
24
28
|
*
|
|
@@ -46,20 +50,27 @@
|
|
|
46
50
|
* })
|
|
47
51
|
*
|
|
48
52
|
*
|
|
49
|
-
* 3.
|
|
53
|
+
* 3. GROUPED CASES - share one handler between several values with
|
|
54
|
+
* `typedSwitchGroup`. The handler receives the union of the grouped
|
|
55
|
+
* members, and the spread keeps exhaustiveness and return type inference:
|
|
50
56
|
*
|
|
51
57
|
* @example
|
|
52
|
-
*
|
|
58
|
+
* typedSwitch(event, 'type', {
|
|
59
|
+
* ...typedSwitchGroup(event, 'type', ['click', 'scroll'], (e) => e.type), // e: ClickEvent | ScrollEvent
|
|
60
|
+
* key: (e) => e.key,
|
|
61
|
+
* })
|
|
53
62
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* error: () => ({ id: '456' }), // ✓ OK
|
|
63
|
+
* typedSwitch(status, {
|
|
64
|
+
* ...typedSwitchGroup(status, ['success', 'pending'], (s) => 'fine'), // s: 'success' | 'pending'
|
|
65
|
+
* error: () => 'failed',
|
|
58
66
|
* })
|
|
59
67
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
68
|
+
*
|
|
69
|
+
* 4. ENFORCING A RETURN TYPE - annotate the result:
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* const result: { id: string } = typedSwitch(status, {
|
|
73
|
+
* success: () => ({ id: '123' }),
|
|
63
74
|
* error: () => ({ id: '456' }),
|
|
64
75
|
* })
|
|
65
76
|
*/
|
|
@@ -145,6 +156,16 @@ export type PartialObjectSwitchCases<
|
|
|
145
156
|
R
|
|
146
157
|
> = T[K] extends Discriminant ? PartialObjectCases<T, K, R | Promise<R>> : never
|
|
147
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Rejects case keys that are not values of the switched union, so a typo
|
|
161
|
+
* fails to compile instead of silently falling through to the default.
|
|
162
|
+
*/
|
|
163
|
+
type NoUnknownCases<Cases, Allowed extends string> = {
|
|
164
|
+
[P in keyof Cases as `${P & (string | number)}` extends Allowed
|
|
165
|
+
? never
|
|
166
|
+
: P]: never
|
|
167
|
+
}
|
|
168
|
+
|
|
148
169
|
type Handler = (value: unknown) => unknown
|
|
149
170
|
type CasesRecord = Record<string, Handler | undefined>
|
|
150
171
|
|
|
@@ -152,18 +173,25 @@ type CasesRecord = Record<string, Handler | undefined>
|
|
|
152
173
|
// Function overloads
|
|
153
174
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
154
175
|
|
|
155
|
-
//
|
|
176
|
+
// Plain value input - all cases required (no default)
|
|
156
177
|
export function typedSwitch<
|
|
157
|
-
T extends
|
|
158
|
-
Cases extends { [
|
|
178
|
+
T extends Discriminant,
|
|
179
|
+
Cases extends { [V in T as `${V}`]: (value: V) => unknown } & NoUnknownCases<
|
|
180
|
+
Cases,
|
|
181
|
+
`${T}`
|
|
182
|
+
>
|
|
159
183
|
>(value: T, cases: Cases): RawCasesReturnType<Cases>
|
|
160
184
|
|
|
161
|
-
//
|
|
185
|
+
// Plain value input - partial cases allowed (with default)
|
|
162
186
|
// The default handler only receives the cases not covered by `cases`.
|
|
163
187
|
export function typedSwitch<
|
|
164
|
-
T extends
|
|
165
|
-
Cases extends {
|
|
166
|
-
|
|
188
|
+
T extends Discriminant,
|
|
189
|
+
Cases extends {
|
|
190
|
+
[V in T as `${V}`]?: (value: V) => unknown
|
|
191
|
+
} & NoUnknownCases<Cases, `${T}`>,
|
|
192
|
+
DefaultCase extends (
|
|
193
|
+
value: Exclude<T, ValuesForKeys<T, HandledCaseKeys<Cases>>>
|
|
194
|
+
) => unknown
|
|
167
195
|
>(
|
|
168
196
|
value: T,
|
|
169
197
|
cases: Cases,
|
|
@@ -174,7 +202,8 @@ export function typedSwitch<
|
|
|
174
202
|
export function typedSwitch<
|
|
175
203
|
T extends object,
|
|
176
204
|
K extends keyof T,
|
|
177
|
-
Cases extends ObjectCases<T, K, unknown>
|
|
205
|
+
Cases extends ObjectCases<T, K, unknown> &
|
|
206
|
+
NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>
|
|
178
207
|
>(value: T, key: K, cases: Cases): RawCasesReturnType<Cases>
|
|
179
208
|
|
|
180
209
|
// Object input with key - partial cases allowed (with default)
|
|
@@ -182,7 +211,8 @@ export function typedSwitch<
|
|
|
182
211
|
export function typedSwitch<
|
|
183
212
|
T extends object,
|
|
184
213
|
K extends keyof T,
|
|
185
|
-
Cases extends PartialObjectCases<T, K, unknown
|
|
214
|
+
Cases extends PartialObjectCases<T, K, unknown> &
|
|
215
|
+
NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>,
|
|
186
216
|
DefaultCase extends (
|
|
187
217
|
value: Exclude<T, Record<K, ValuesForKeys<T[K], HandledCaseKeys<Cases>>>>
|
|
188
218
|
) => unknown
|
|
@@ -193,69 +223,9 @@ export function typedSwitch<
|
|
|
193
223
|
defaultCase: DefaultCase
|
|
194
224
|
): RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>
|
|
195
225
|
|
|
196
|
-
// String input - all cases required with return constraint (no default)
|
|
197
|
-
export function typedSwitch<Constraint, T extends string = string>(
|
|
198
|
-
value: T,
|
|
199
|
-
cases: { [K in T]: (value: K) => Constraint }
|
|
200
|
-
): Constraint
|
|
201
|
-
export function typedSwitch<Constraint, T extends string = string>(
|
|
202
|
-
value: T,
|
|
203
|
-
cases: { [K in T]: (value: K) => Constraint | Promise<Constraint> }
|
|
204
|
-
): Constraint | Promise<Constraint>
|
|
205
|
-
|
|
206
|
-
// String input - partial cases allowed with return constraint (with default)
|
|
207
|
-
export function typedSwitch<Constraint, T extends string = string>(
|
|
208
|
-
value: T,
|
|
209
|
-
cases: { [K in T]?: (value: K) => Constraint },
|
|
210
|
-
defaultCase: (value: T) => Constraint
|
|
211
|
-
): Constraint
|
|
212
|
-
export function typedSwitch<Constraint, T extends string = string>(
|
|
213
|
-
value: T,
|
|
214
|
-
cases: { [K in T]?: (value: K) => Constraint | Promise<Constraint> },
|
|
215
|
-
defaultCase: (value: T) => Constraint | Promise<Constraint>
|
|
216
|
-
): Constraint | Promise<Constraint>
|
|
217
|
-
|
|
218
|
-
// Object input with key - all cases required with return constraint (no default)
|
|
219
|
-
export function typedSwitch<
|
|
220
|
-
Constraint,
|
|
221
|
-
T extends Record<string, unknown> = Record<string, unknown>,
|
|
222
|
-
K extends keyof T = keyof T
|
|
223
|
-
>(value: T, key: K, cases: ObjectCases<T, K, Constraint>): Constraint
|
|
224
|
-
export function typedSwitch<
|
|
225
|
-
Constraint,
|
|
226
|
-
T extends Record<string, unknown> = Record<string, unknown>,
|
|
227
|
-
K extends keyof T = keyof T
|
|
228
|
-
>(
|
|
229
|
-
value: T,
|
|
230
|
-
key: K,
|
|
231
|
-
cases: ObjectCases<T, K, Constraint | Promise<Constraint>>
|
|
232
|
-
): Constraint | Promise<Constraint>
|
|
233
|
-
|
|
234
|
-
// Object input with key - partial cases allowed with return constraint (with default)
|
|
235
|
-
export function typedSwitch<
|
|
236
|
-
Constraint,
|
|
237
|
-
T extends Record<string, unknown> = Record<string, unknown>,
|
|
238
|
-
K extends keyof T = keyof T
|
|
239
|
-
>(
|
|
240
|
-
value: T,
|
|
241
|
-
key: K,
|
|
242
|
-
cases: PartialObjectCases<T, K, Constraint>,
|
|
243
|
-
defaultCase: (value: T) => Constraint
|
|
244
|
-
): Constraint
|
|
245
|
-
export function typedSwitch<
|
|
246
|
-
Constraint,
|
|
247
|
-
T extends Record<string, unknown> = Record<string, unknown>,
|
|
248
|
-
K extends keyof T = keyof T
|
|
249
|
-
>(
|
|
250
|
-
value: T,
|
|
251
|
-
key: K,
|
|
252
|
-
cases: PartialObjectCases<T, K, Constraint | Promise<Constraint>>,
|
|
253
|
-
defaultCase: (value: T) => Constraint | Promise<Constraint>
|
|
254
|
-
): Constraint | Promise<Constraint>
|
|
255
|
-
|
|
256
226
|
// Implementation
|
|
257
227
|
export function typedSwitch(
|
|
258
|
-
value:
|
|
228
|
+
value: Discriminant | object,
|
|
259
229
|
keyOrCases: string | CasesRecord,
|
|
260
230
|
casesOrDefault?: CasesRecord | Handler,
|
|
261
231
|
defaultCase?: Handler
|
|
@@ -265,9 +235,9 @@ export function typedSwitch(
|
|
|
265
235
|
let defaultHandler: Handler | undefined
|
|
266
236
|
let discriminantKey: string | undefined
|
|
267
237
|
|
|
268
|
-
if (typeof value
|
|
269
|
-
//
|
|
270
|
-
key = value
|
|
238
|
+
if (typeof value !== 'object') {
|
|
239
|
+
// Plain value input: typedSwitch(value, cases, default?)
|
|
240
|
+
key = String(value)
|
|
271
241
|
cases = keyOrCases as CasesRecord
|
|
272
242
|
defaultHandler = casesOrDefault as Handler | undefined
|
|
273
243
|
} else {
|
|
@@ -323,6 +293,50 @@ export function typedSwitch(
|
|
|
323
293
|
)
|
|
324
294
|
}
|
|
325
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Builds cases that share one handler, for spreading into `typedSwitch` cases.
|
|
298
|
+
* Pass the switched value so its type is inferred; the handler receives the
|
|
299
|
+
* union of the grouped members. A case listed after the spread replaces the
|
|
300
|
+
* grouped handler for that key; listing it before the spread is a type error.
|
|
301
|
+
* If two groups list the same key, the later group wins.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* typedSwitch(event, 'type', {
|
|
305
|
+
* ...typedSwitchGroup(event, 'type', ['click', 'scroll'], (e) => e.type),
|
|
306
|
+
* key: (e) => e.key,
|
|
307
|
+
* })
|
|
308
|
+
*/
|
|
309
|
+
export function typedSwitchGroup<
|
|
310
|
+
T extends Discriminant,
|
|
311
|
+
const Keys extends readonly T[],
|
|
312
|
+
R
|
|
313
|
+
>(
|
|
314
|
+
value: T,
|
|
315
|
+
keys: Keys,
|
|
316
|
+
handler: (value: Keys[number]) => R
|
|
317
|
+
): { [V in Keys[number] as `${V}`]: (value: V) => R }
|
|
318
|
+
export function typedSwitchGroup<
|
|
319
|
+
T extends object,
|
|
320
|
+
K extends keyof T,
|
|
321
|
+
const Keys extends readonly Extract<T[K], Discriminant>[],
|
|
322
|
+
R
|
|
323
|
+
>(
|
|
324
|
+
value: T,
|
|
325
|
+
key: K,
|
|
326
|
+
keys: Keys,
|
|
327
|
+
handler: (value: Extract<T, Record<K, Keys[number]>>) => R
|
|
328
|
+
): { [V in Keys[number] as `${V}`]: (value: Extract<T, Record<K, V>>) => R }
|
|
329
|
+
export function typedSwitchGroup(
|
|
330
|
+
_value: unknown,
|
|
331
|
+
keyOrKeys: unknown,
|
|
332
|
+
keysOrHandler: unknown,
|
|
333
|
+
handler?: Handler
|
|
334
|
+
): CasesRecord {
|
|
335
|
+
const keys = (handler ? keysOrHandler : keyOrKeys) as Discriminant[]
|
|
336
|
+
const shared = handler ?? (keysOrHandler as Handler)
|
|
337
|
+
return Object.fromEntries(keys.map((key) => [String(key), shared]))
|
|
338
|
+
}
|
|
339
|
+
|
|
326
340
|
/**
|
|
327
341
|
* Exhaustiveness helper for regular `switch` statements. Place it in the
|
|
328
342
|
* `default` branch: it only type-checks when every case has been handled, and
|