hanzio 2.2.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/README.md +19 -4
- package/dist/src/index.js +8 -2
- package/dist/typedswitch/index.d.ts +55 -34
- package/package.json +2 -1
- package/src/api-wrapper/README.md +5 -2
- package/src/api-wrapper/SKILL.md +256 -0
- package/src/typedswitch/README.md +55 -39
- package/src/typedswitch/index.ts +97 -83
package/README.md
CHANGED
|
@@ -52,6 +52,15 @@ subpath export:
|
|
|
52
52
|
`scripts/verify-exports.mjs` imports every entry above from the packed tarball
|
|
53
53
|
in CI, so this table and `package.json#exports` can't drift apart.
|
|
54
54
|
|
|
55
|
+
## Migrating to 3.0
|
|
56
|
+
|
|
57
|
+
- `typedSwitch<Constraint>(…)` is removed. The explicit type argument switched
|
|
58
|
+
off inference, so those calls checked neither exhaustiveness nor, in object
|
|
59
|
+
mode, the return type. Annotate the result instead:
|
|
60
|
+
`const result: HasId = typedSwitch(status, { … })`.
|
|
61
|
+
- `typedSwitch` rejects case keys that aren't values of the switched union. A
|
|
62
|
+
typo used to compile and fall through to the default handler.
|
|
63
|
+
|
|
55
64
|
## Migrating to 2.0
|
|
56
65
|
|
|
57
66
|
- Subpath-only modules are no longer re-exported from `hanzio`. Change
|
|
@@ -479,12 +488,13 @@ Validators run in order: `globalValidator`, `validators[toState]`, then
|
|
|
479
488
|
|
|
480
489
|
### typedSwitch
|
|
481
490
|
|
|
482
|
-
`typedSwitch` is an exhaustive switch helper for string
|
|
483
|
-
|
|
484
|
-
`switch` statements. See the
|
|
491
|
+
`typedSwitch` is an exhaustive switch helper for string, number and boolean
|
|
492
|
+
values and for discriminated unions. `typedSwitchGroup` lets several cases share
|
|
493
|
+
one handler. `assertNever` covers regular `switch` statements. See the
|
|
494
|
+
[typedSwitch docs](src/typedswitch/README.md).
|
|
485
495
|
|
|
486
496
|
```ts
|
|
487
|
-
import { assertNever, typedSwitch } from 'hanzio'
|
|
497
|
+
import { assertNever, typedSwitch, typedSwitchGroup } from 'hanzio'
|
|
488
498
|
|
|
489
499
|
type Event =
|
|
490
500
|
| { type: 'click'; x: number; y: number }
|
|
@@ -501,6 +511,11 @@ const text = typedSwitch(response as Response, 'code', {
|
|
|
501
511
|
404: () => 'Not found'
|
|
502
512
|
})
|
|
503
513
|
|
|
514
|
+
const event = { type: 'scroll', offset: 4 } as Event
|
|
515
|
+
typedSwitch(event, 'type', {
|
|
516
|
+
...typedSwitchGroup(event, 'type', ['click', 'scroll'], (e) => e.type)
|
|
517
|
+
})
|
|
518
|
+
|
|
504
519
|
function area(shape: Shape) {
|
|
505
520
|
switch (shape.kind) {
|
|
506
521
|
case 'circle':
|
package/dist/src/index.js
CHANGED
|
@@ -90,8 +90,8 @@ function typedSwitch(value, keyOrCases, casesOrDefault, defaultCase) {
|
|
|
90
90
|
let cases;
|
|
91
91
|
let defaultHandler;
|
|
92
92
|
let discriminantKey;
|
|
93
|
-
if (typeof value
|
|
94
|
-
key = value;
|
|
93
|
+
if (typeof value !== "object") {
|
|
94
|
+
key = String(value);
|
|
95
95
|
cases = keyOrCases;
|
|
96
96
|
defaultHandler = casesOrDefault;
|
|
97
97
|
} else {
|
|
@@ -119,6 +119,11 @@ function typedSwitch(value, keyOrCases, casesOrDefault, defaultCase) {
|
|
|
119
119
|
const contextSuffix = discriminantKey ? ` (discriminant key: "${discriminantKey}")` : "";
|
|
120
120
|
throw new Error(`Unhandled case: ${key}${contextSuffix}.${availableCasesSuffix}`);
|
|
121
121
|
}
|
|
122
|
+
function typedSwitchGroup(_value, keyOrKeys, keysOrHandler, handler) {
|
|
123
|
+
const keys = handler ? keysOrHandler : keyOrKeys;
|
|
124
|
+
const shared = handler ?? keysOrHandler;
|
|
125
|
+
return Object.fromEntries(keys.map((key) => [String(key), shared]));
|
|
126
|
+
}
|
|
122
127
|
function assertNever(value, message) {
|
|
123
128
|
throw new Error(message ?? `Unexpected value: ${String(value)}`);
|
|
124
129
|
}
|
|
@@ -1093,6 +1098,7 @@ export {
|
|
|
1093
1098
|
typedFromEntries,
|
|
1094
1099
|
typedKeys,
|
|
1095
1100
|
typedSwitch,
|
|
1101
|
+
typedSwitchGroup,
|
|
1096
1102
|
unwrapResult2 as unwrapResult,
|
|
1097
1103
|
withAlpha,
|
|
1098
1104
|
withTimeout2 as withTimeout
|
|
@@ -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
|
*/
|
|
@@ -95,30 +106,40 @@ type PartialObjectCases<T, K extends keyof T, R> = {
|
|
|
95
106
|
export type ObjectSwitchCases<T, K extends keyof T, R> = T[K] extends Discriminant ? ObjectCases<T, K, R | Promise<R>> : never;
|
|
96
107
|
/** Partial cases for object input with custom key (when default is provided) */
|
|
97
108
|
export type PartialObjectSwitchCases<T, K extends keyof T, R> = T[K] extends Discriminant ? PartialObjectCases<T, K, R | Promise<R>> : never;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
export declare function typedSwitch<T extends
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
export declare function typedSwitch<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Rejects case keys that are not values of the switched union, so a typo
|
|
111
|
+
* fails to compile instead of silently falling through to the default.
|
|
112
|
+
*/
|
|
113
|
+
type NoUnknownCases<Cases, Allowed extends string> = {
|
|
114
|
+
[P in keyof Cases as `${P & (string | number)}` extends Allowed ? never : P]: never;
|
|
115
|
+
};
|
|
116
|
+
export declare function typedSwitch<T extends Discriminant, Cases extends {
|
|
117
|
+
[V in T as `${V}`]: (value: V) => unknown;
|
|
118
|
+
} & NoUnknownCases<Cases, `${T}`>>(value: T, cases: Cases): RawCasesReturnType<Cases>;
|
|
119
|
+
export declare function typedSwitch<T extends Discriminant, Cases extends {
|
|
120
|
+
[V in T as `${V}`]?: (value: V) => unknown;
|
|
121
|
+
} & NoUnknownCases<Cases, `${T}`>, DefaultCase extends (value: Exclude<T, ValuesForKeys<T, HandledCaseKeys<Cases>>>) => unknown>(value: T, cases: Cases, defaultCase: DefaultCase): RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>;
|
|
122
|
+
export declare function typedSwitch<T extends object, K extends keyof T, Cases extends ObjectCases<T, K, unknown> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>>(value: T, key: K, cases: Cases): RawCasesReturnType<Cases>;
|
|
123
|
+
export declare function typedSwitch<T extends object, K extends keyof T, Cases extends PartialObjectCases<T, K, unknown> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>, DefaultCase extends (value: Exclude<T, Record<K, ValuesForKeys<T[K], HandledCaseKeys<Cases>>>>) => unknown>(value: T, key: K, cases: Cases, defaultCase: DefaultCase): RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>;
|
|
124
|
+
/**
|
|
125
|
+
* Builds cases that share one handler, for spreading into `typedSwitch` cases.
|
|
126
|
+
* Pass the switched value so its type is inferred; the handler receives the
|
|
127
|
+
* union of the grouped members. A case listed after the spread replaces the
|
|
128
|
+
* grouped handler for that key; listing it before the spread is a type error.
|
|
129
|
+
* If two groups list the same key, the later group wins.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* typedSwitch(event, 'type', {
|
|
133
|
+
* ...typedSwitchGroup(event, 'type', ['click', 'scroll'], (e) => e.type),
|
|
134
|
+
* key: (e) => e.key,
|
|
135
|
+
* })
|
|
136
|
+
*/
|
|
137
|
+
export declare function typedSwitchGroup<T extends Discriminant, const Keys extends readonly T[], R>(value: T, keys: Keys, handler: (value: Keys[number]) => R): {
|
|
138
|
+
[V in Keys[number] as `${V}`]: (value: V) => R;
|
|
139
|
+
};
|
|
140
|
+
export declare function typedSwitchGroup<T extends object, K extends keyof T, const Keys extends readonly Extract<T[K], Discriminant>[], R>(value: T, key: K, keys: Keys, handler: (value: Extract<T, Record<K, Keys[number]>>) => R): {
|
|
141
|
+
[V in Keys[number] as `${V}`]: (value: Extract<T, Record<K, V>>) => R;
|
|
142
|
+
};
|
|
122
143
|
/**
|
|
123
144
|
* Exhaustiveness helper for regular `switch` statements. Place it in the
|
|
124
145
|
* `default` branch: it only type-checks when every case has been handled, and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hanzio",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "A lightweight set of utility functions for TypeScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/src/index.js",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"src/**/*.ts",
|
|
28
28
|
"src/**/README.md",
|
|
29
|
+
"src/api-wrapper/SKILL.md",
|
|
29
30
|
"dist",
|
|
30
31
|
"!src/**/*.test.ts"
|
|
31
32
|
],
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
Typed HTTP clients from Zod schemas: `createApiClient` validates requests and
|
|
4
4
|
responses, retries with exponential backoff, and throws structured errors.
|
|
5
5
|
|
|
6
|
-
Building a new wrapper with an AI agent?
|
|
7
|
-
step-by-step workflow and folder conventions.
|
|
6
|
+
Building a new wrapper with an AI agent? [SKILL.md](./SKILL.md) has the
|
|
7
|
+
step-by-step workflow and folder conventions. It ships with the package: copy
|
|
8
|
+
`node_modules/hanzio/src/api-wrapper/SKILL.md` to
|
|
9
|
+
`.claude/skills/hanzio-api-wrapper/SKILL.md` (or `.agents/skills/…`), or point
|
|
10
|
+
your agent at it.
|
|
8
11
|
|
|
9
12
|
## Quick Start
|
|
10
13
|
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hanzio-api-wrapper
|
|
3
|
+
description: Building typed HTTP API wrappers with createApiClient and Zod in hanzio/api-wrapper. Covers folder layout, index factory, endpoint files with defineEndpoint, shared types, pagination with paginate, and common REST API integration patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AI Agent Guide: Building API Wrappers
|
|
7
|
+
|
|
8
|
+
This guide is for AI assistants to follow when building API wrappers using the
|
|
9
|
+
`createApiClient` utility from `hanzio/api-wrapper`. The API reference
|
|
10
|
+
(configuration, errors, actions, retries, pagination) lives in
|
|
11
|
+
[README.md](./README.md).
|
|
12
|
+
|
|
13
|
+
## Folder Structure
|
|
14
|
+
|
|
15
|
+
When creating a new API wrapper, follow this structure:
|
|
16
|
+
|
|
17
|
+
```txt
|
|
18
|
+
src/api-wrapper/{api-name}/
|
|
19
|
+
├── index.ts
|
|
20
|
+
├── types.ts
|
|
21
|
+
├── {endpointName}.ts
|
|
22
|
+
├── {anotherEndpoint}.ts
|
|
23
|
+
└── utils/
|
|
24
|
+
├── {utilName}.ts
|
|
25
|
+
└── ...
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Naming Conventions
|
|
29
|
+
|
|
30
|
+
- Folder name: lowercase, kebab-case, for example `github-api`, `stripe`, `template-persona`.
|
|
31
|
+
- Endpoint files: camelCase and action-oriented, for example `topicsGet.ts`, `usersCreate.ts`, `ordersUpdate.ts`.
|
|
32
|
+
- Utility files: camelCase and descriptive, for example `paginateResults.ts`, `templateGetAll.ts`.
|
|
33
|
+
|
|
34
|
+
## Step-By-Step Process
|
|
35
|
+
|
|
36
|
+
### Step 1: Gather Information
|
|
37
|
+
|
|
38
|
+
The user will typically provide one or more of:
|
|
39
|
+
|
|
40
|
+
1. cURL command: extract method, URL, headers, and body.
|
|
41
|
+
2. Request/response examples: use them to build Zod schemas.
|
|
42
|
+
3. Chrome DevTools Network logs: extract request and response details.
|
|
43
|
+
4. API documentation: reference schemas, auth, pagination, and endpoints.
|
|
44
|
+
|
|
45
|
+
### Step 2: Create The Folder
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
mkdir -p src/api-wrapper/{api-name}/utils
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Step 3: Create The Index File
|
|
52
|
+
|
|
53
|
+
The index file creates and exports the API client. When the token comes from
|
|
54
|
+
a secret store, create the client at module level and resolve the header
|
|
55
|
+
lazily with a `defaultHeaders` function; take the token as a parameter only
|
|
56
|
+
when callers supply it per instance.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { createApiClient } from 'hanzio/api-wrapper'
|
|
60
|
+
import { endpointOne } from './endpointOne'
|
|
61
|
+
import { endpointTwo } from './endpointTwo'
|
|
62
|
+
|
|
63
|
+
export const exampleApi = createApiClient({
|
|
64
|
+
name: 'exampleApi',
|
|
65
|
+
baseApiUrls: {
|
|
66
|
+
default: 'https://api.example.com'
|
|
67
|
+
},
|
|
68
|
+
defaultHeaders: () => ({
|
|
69
|
+
Authorization: `Bearer ${getSecret('EXAMPLE_API_TOKEN')}`
|
|
70
|
+
}),
|
|
71
|
+
endpoints: {
|
|
72
|
+
endpointOne,
|
|
73
|
+
endpointTwo
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If the project uses OpenTelemetry, add
|
|
79
|
+
`use: [otelMiddleware(trace.getTracer('app'))]`; for other shared middleware
|
|
80
|
+
(metrics), pass it with `use: [...]` instead of wrapping `fetch`. Pass a
|
|
81
|
+
`logger` so failures are logged with their category (server, input or output
|
|
82
|
+
validation). If the API returns HTTP 200 with an
|
|
83
|
+
error envelope (e.g. `{ success: false, errors }`), add a `checkResponse` that
|
|
84
|
+
throws on failure rather than checking after every call. Endpoints without the
|
|
85
|
+
envelope set `checkResponse: false` (or their own check). When the result is in
|
|
86
|
+
response headers (e.g. a `Location`), read them in `resFormatter(body, headers)`.
|
|
87
|
+
|
|
88
|
+
### Step 4: Create Shared Types
|
|
89
|
+
|
|
90
|
+
If multiple endpoints share schemas, create a `types.ts` file.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { z } from 'zod'
|
|
94
|
+
|
|
95
|
+
export const ListQueryInput = z.object({
|
|
96
|
+
page: z.number().optional(),
|
|
97
|
+
page_size: z.number().optional()
|
|
98
|
+
})
|
|
99
|
+
export type ListQueryInput = z.infer<typeof ListQueryInput>
|
|
100
|
+
|
|
101
|
+
export const ListQueryOutput = <T extends z.ZodType>(itemSchema: T) =>
|
|
102
|
+
z.object({
|
|
103
|
+
data: z.array(itemSchema),
|
|
104
|
+
current_page: z.number(),
|
|
105
|
+
last_page: z.number(),
|
|
106
|
+
per_page: z.number(),
|
|
107
|
+
total: z.number()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
export type ListQueryOutput<T> = {
|
|
111
|
+
data: T[]
|
|
112
|
+
current_page: number
|
|
113
|
+
last_page: number
|
|
114
|
+
per_page: number
|
|
115
|
+
total: number
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Step 5: Create Endpoint Files
|
|
120
|
+
|
|
121
|
+
Each endpoint gets its own file with request/response schemas and endpoint
|
|
122
|
+
configuration.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { defineEndpoint } from 'hanzio/api-wrapper'
|
|
126
|
+
import { z } from 'zod'
|
|
127
|
+
|
|
128
|
+
export const UserResponse = z.object({
|
|
129
|
+
id: z.number(),
|
|
130
|
+
name: z.string(),
|
|
131
|
+
email: z.string()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
export const usersGet = defineEndpoint({
|
|
135
|
+
method: 'GET',
|
|
136
|
+
path: '/users/:userId',
|
|
137
|
+
reqQuerySchema: z.object({ includeInactive: z.boolean().optional() }),
|
|
138
|
+
resSchema: UserResponse
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`defineEndpoint` types `reqParams` from the `:params` in `path`, so add a
|
|
143
|
+
`reqParamsSchema` only when params need validation or coercion.
|
|
144
|
+
|
|
145
|
+
### Step 6: Create Utility Functions
|
|
146
|
+
|
|
147
|
+
Place reusable helpers in `utils/`, e.g. response normalization or rate-limit
|
|
148
|
+
handling. For pagination, use the built-in `paginate` instead of a hand-written
|
|
149
|
+
loop:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { paginate } from 'hanzio/api-wrapper'
|
|
153
|
+
import type { GetExampleApi } from '../index'
|
|
154
|
+
|
|
155
|
+
export const usersGetAll = (api: GetExampleApi) =>
|
|
156
|
+
Array.fromAsync(
|
|
157
|
+
paginate(
|
|
158
|
+
(page: number = 1) => api.usersList({ reqQuery: { page } }),
|
|
159
|
+
(res) =>
|
|
160
|
+
res.data.current_page < res.data.last_page
|
|
161
|
+
? res.data.current_page + 1
|
|
162
|
+
: null
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Extracting Information From User Input
|
|
168
|
+
|
|
169
|
+
### From cURL Commands
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
curl -X POST 'https://api.example.com/users' \
|
|
173
|
+
-H 'Authorization: Bearer token123' \
|
|
174
|
+
-H 'Content-Type: application/json' \
|
|
175
|
+
-d '{"name": "John", "email": "john@example.com"}'
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Extract:
|
|
179
|
+
|
|
180
|
+
- Method: `POST`
|
|
181
|
+
- Base URL: `https://api.example.com`
|
|
182
|
+
- Path: `/users`
|
|
183
|
+
- Headers: `Authorization`, `Content-Type`
|
|
184
|
+
- Body schema: `{ name: string, email: string }`
|
|
185
|
+
|
|
186
|
+
### From Chrome DevTools
|
|
187
|
+
|
|
188
|
+
Look for:
|
|
189
|
+
|
|
190
|
+
1. Request URL: base URL plus path.
|
|
191
|
+
2. Request method: HTTP method.
|
|
192
|
+
3. Request headers: auth and custom headers.
|
|
193
|
+
4. Request payload: body schema.
|
|
194
|
+
5. Response body: response schema.
|
|
195
|
+
6. Query string parameters: query schema.
|
|
196
|
+
|
|
197
|
+
### Building Zod Schemas From JSON
|
|
198
|
+
|
|
199
|
+
Given this response:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"id": 123,
|
|
204
|
+
"name": "John Doe",
|
|
205
|
+
"email": "john@example.com",
|
|
206
|
+
"created_at": "2024-01-15T10:30:00Z",
|
|
207
|
+
"roles": ["admin", "user"],
|
|
208
|
+
"profile": {
|
|
209
|
+
"avatar_url": "https://example.com/avatar.png",
|
|
210
|
+
"bio": "Developer"
|
|
211
|
+
},
|
|
212
|
+
"is_active": true
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Create this schema:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const UserResponse = z.object({
|
|
220
|
+
id: z.number(),
|
|
221
|
+
name: z.string(),
|
|
222
|
+
email: z.string(),
|
|
223
|
+
created_at: z.string(),
|
|
224
|
+
roles: z.array(z.string()),
|
|
225
|
+
profile: z.object({
|
|
226
|
+
avatar_url: z.string(),
|
|
227
|
+
bio: z.string()
|
|
228
|
+
}),
|
|
229
|
+
is_active: z.boolean()
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Checklist
|
|
234
|
+
|
|
235
|
+
- [ ] Create folder with API name in kebab-case.
|
|
236
|
+
- [ ] Create `index.ts` with a `createApiClient` call.
|
|
237
|
+
- [ ] Set `baseApiUrls` and `defaultHeaders` (a function when the token comes from a secret store).
|
|
238
|
+
- [ ] Add `checkResponse` if the API returns errors inside 200 responses.
|
|
239
|
+
- [ ] Create `types.ts` for shared schemas when needed.
|
|
240
|
+
- [ ] Create one file per endpoint.
|
|
241
|
+
- [ ] Define Zod schemas for all request and response data.
|
|
242
|
+
- [ ] Use `defineEndpoint` for endpoint definitions.
|
|
243
|
+
- [ ] Create helpers in `utils/` when needed.
|
|
244
|
+
- [ ] Export the API type, for example `export type ExampleApi = typeof exampleApi`.
|
|
245
|
+
- [ ] Test with `createMockFetch` from `hanzio/api-wrapper/testing` passed as `fetch`; don't replace `globalThis.fetch`.
|
|
246
|
+
|
|
247
|
+
## Tips
|
|
248
|
+
|
|
249
|
+
1. Always use `defineEndpoint` for endpoint definitions (it checks `resFormatter` and types path params).
|
|
250
|
+
2. Import Zod from `zod`.
|
|
251
|
+
3. Be strict with schemas and mark unstable fields as `.optional()` or `.nullish()`.
|
|
252
|
+
4. Use descriptive endpoint names like `usersGet`, `usersCreate`, and `ordersListByStatus`.
|
|
253
|
+
5. Keep files small by using one endpoint per file.
|
|
254
|
+
6. Extract schemas to `types.ts` when used by two or more endpoints.
|
|
255
|
+
7. Use `paginate` when APIs use pagination.
|
|
256
|
+
8. Ask for sample responses if the user only provides cURL commands.
|
|
@@ -22,15 +22,16 @@ bun add typedswitch
|
|
|
22
22
|
- **Exhaustiveness checking** — TypeScript ensures all cases are handled at compile time
|
|
23
23
|
- **Discriminated union support** — Works with any discriminant key, not just `type`
|
|
24
24
|
- **Async-aware** — Async handlers preserve their `Promise` return type
|
|
25
|
+
- **Grouped cases** — Share one handler between several values, fully typed
|
|
25
26
|
- **Default handlers** — Handle remaining cases with a fallback
|
|
26
|
-
- **
|
|
27
|
+
- **Typo-proof cases** — A case key that isn't a value of the union is a compile error
|
|
27
28
|
- **`assertNever`** — Exhaustiveness checking for regular `switch` statements
|
|
28
29
|
|
|
29
30
|
## Usage
|
|
30
31
|
|
|
31
|
-
###
|
|
32
|
+
### Plain Value Input
|
|
32
33
|
|
|
33
|
-
Switch on a string
|
|
34
|
+
Switch on a string, number, boolean or enum value directly:
|
|
34
35
|
|
|
35
36
|
```typescript
|
|
36
37
|
import { typedSwitch } from 'typedswitch'
|
|
@@ -58,6 +59,21 @@ const result = typedSwitch(status, {
|
|
|
58
59
|
})
|
|
59
60
|
```
|
|
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
|
+
|
|
61
77
|
### Discriminated Union Input
|
|
62
78
|
|
|
63
79
|
Switch on objects with a string, number, or boolean discriminant property (like `type`, `kind`, `status`, etc.):
|
|
@@ -116,6 +132,32 @@ typedSwitch(result, 'ok', {
|
|
|
116
132
|
})
|
|
117
133
|
```
|
|
118
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
|
+
|
|
119
161
|
### Default Handler
|
|
120
162
|
|
|
121
163
|
Handle a subset of cases and catch the rest with a default handler:
|
|
@@ -202,45 +244,23 @@ const result = typedSwitch(status, {
|
|
|
202
244
|
// result: { ok: true; data: string } | { ok: false; code: number } | null
|
|
203
245
|
```
|
|
204
246
|
|
|
205
|
-
### Return Type
|
|
247
|
+
### Enforcing a Return Type
|
|
206
248
|
|
|
207
|
-
|
|
249
|
+
Annotate the result. Every handler's return type must fit, and exhaustiveness
|
|
250
|
+
checking still applies:
|
|
208
251
|
|
|
209
252
|
```typescript
|
|
210
253
|
interface HasId {
|
|
211
254
|
id: string
|
|
212
255
|
}
|
|
213
256
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
error: () => ({ id: '456', code: 500 }), // ✓ OK
|
|
218
|
-
pending: () => ({ id: '789' }), // ✓ OK
|
|
219
|
-
})
|
|
220
|
-
// result: HasId
|
|
221
|
-
|
|
222
|
-
// This would be a compile error:
|
|
223
|
-
typedSwitch<HasId>(status, {
|
|
224
|
-
success: () => ({ name: 'John' }), // ✗ Error: missing 'id'
|
|
225
|
-
error: () => ({ id: '456' }),
|
|
257
|
+
const result: HasId = typedSwitch(status, {
|
|
258
|
+
success: () => ({ id: '123', name: 'John' }),
|
|
259
|
+
error: () => ({ id: '456', code: 500 }),
|
|
226
260
|
pending: () => ({ id: '789' }),
|
|
227
261
|
})
|
|
228
262
|
```
|
|
229
263
|
|
|
230
|
-
Constraints also work with discriminated unions and default handlers:
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
const result = typedSwitch<HasId>(
|
|
234
|
-
event,
|
|
235
|
-
'type',
|
|
236
|
-
{
|
|
237
|
-
click: () => ({ id: 'click-1', x: 10, y: 20 }),
|
|
238
|
-
scroll: () => ({ id: 'scroll-1', offset: 100 }),
|
|
239
|
-
keypress: () => ({ id: 'key-1', key: 'Enter' }),
|
|
240
|
-
}
|
|
241
|
-
)
|
|
242
|
-
```
|
|
243
|
-
|
|
244
264
|
## Error Handling
|
|
245
265
|
|
|
246
266
|
When a case is unhandled at runtime (e.g., due to unsafe casts), `typedSwitch` throws a descriptive error:
|
|
@@ -284,11 +304,11 @@ function label(status: Status) {
|
|
|
284
304
|
|
|
285
305
|
### `typedSwitch(value, cases)`
|
|
286
306
|
|
|
287
|
-
Switch on a string
|
|
307
|
+
Switch on a string, number or boolean value with exhaustive case handling.
|
|
288
308
|
|
|
289
309
|
### `typedSwitch(value, cases, defaultHandler)`
|
|
290
310
|
|
|
291
|
-
Switch on a string
|
|
311
|
+
Switch on a string, number or boolean value with partial cases and a default fallback.
|
|
292
312
|
|
|
293
313
|
### `typedSwitch(object, key, cases)`
|
|
294
314
|
|
|
@@ -298,13 +318,9 @@ Switch on a discriminated union using the specified discriminant key (string, nu
|
|
|
298
318
|
|
|
299
319
|
Switch on a discriminated union with partial cases and a default fallback.
|
|
300
320
|
|
|
301
|
-
### `
|
|
302
|
-
|
|
303
|
-
Switch on a string literal while enforcing that each handler returns `Constraint` or `Promise<Constraint>`.
|
|
304
|
-
|
|
305
|
-
### `typedSwitch<Constraint>(object, key, cases)`
|
|
321
|
+
### `typedSwitchGroup(value, keys, handler)` / `typedSwitchGroup(object, key, keys, handler)`
|
|
306
322
|
|
|
307
|
-
|
|
323
|
+
Returns cases that all call `handler`, for spreading into `typedSwitch` cases.
|
|
308
324
|
|
|
309
325
|
### `assertNever(value, message?)`
|
|
310
326
|
|
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
|