hanzio 3.1.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/format/index.d.ts +6 -3
- package/dist/src/format/index.js +4 -3
- package/dist/src/i18n/cli.js +32 -0
- package/dist/typedswitch/index.d.ts +26 -1
- package/package.json +1 -1
- package/src/format/README.md +7 -0
- package/src/format/index.ts +11 -5
- package/src/i18n/README.md +3 -1
- package/src/i18n/cli.ts +40 -0
- package/src/typedswitch/README.md +20 -9
- package/src/typedswitch/index.ts +84 -8
package/README.md
CHANGED
|
@@ -562,6 +562,12 @@ const text = typedSwitch(response as Response, 'code', {
|
|
|
562
562
|
404: () => 'Not found'
|
|
563
563
|
})
|
|
564
564
|
|
|
565
|
+
// Annotate the result to keep literal return types
|
|
566
|
+
const tone: 'info' | 'danger' = typedSwitch(response as Response, 'code', {
|
|
567
|
+
200: () => 'info',
|
|
568
|
+
404: () => 'danger'
|
|
569
|
+
})
|
|
570
|
+
|
|
565
571
|
const event = { type: 'scroll', offset: 4 } as Event
|
|
566
572
|
typedSwitch(event, 'type', {
|
|
567
573
|
...typedSwitchGroup(event, 'type', ['click', 'scroll'], (e) => e.type)
|
package/dist/format/index.d.ts
CHANGED
|
@@ -140,8 +140,11 @@ declare const DATE_PRESETS: {
|
|
|
140
140
|
* follows the locale: `'D MMM'` is "5 Mar" in en-GB and "Mar 5" in en-US.
|
|
141
141
|
*/
|
|
142
142
|
export type DatePreset = keyof typeof DATE_PRESETS;
|
|
143
|
-
/**
|
|
144
|
-
|
|
143
|
+
/**
|
|
144
|
+
* A date in one of the `DatePreset` layouts, or with any
|
|
145
|
+
* `Intl.DateTimeFormat` options, in local time unless they set `timeZone`.
|
|
146
|
+
*/
|
|
147
|
+
export declare function formatDate(date: DateInput, format: DatePreset | Intl.DateTimeFormatOptions, locale: string): string;
|
|
145
148
|
/** Medium date and short time: "5 Mar 2026, 14:30" (en-GB). */
|
|
146
149
|
export declare function formatDateTime(date: DateInput, locale: string): string;
|
|
147
150
|
/**
|
|
@@ -182,7 +185,7 @@ export declare function pluralize(count: number, forms: Partial<Record<Intl.LDML
|
|
|
182
185
|
* `export const fmt = withLocale(() => uiLocales.tag(appI18n.locale()))`.
|
|
183
186
|
*/
|
|
184
187
|
export declare function withLocale(getLocale: () => string): {
|
|
185
|
-
formatDate: (date: DateInput,
|
|
188
|
+
formatDate: (date: DateInput, format: "D MMM" | "D MMM YYYY" | "DD/MM/YYYY" | "DD/MM/YYYY HH:mm" | "HH:mm" | "MMMM YYYY" | "ddd" | "dddd, MMMM D" | Intl.DateTimeFormatOptions) => string;
|
|
186
189
|
formatDateTime: (date: DateInput) => string;
|
|
187
190
|
formatCompactTime: (date: DateInput) => string;
|
|
188
191
|
formatDayDivider: (date: DateInput) => string;
|
package/dist/src/format/index.js
CHANGED
|
@@ -193,8 +193,9 @@ var DATE_PRESETS = {
|
|
|
193
193
|
"dddd, MMMM D": { weekday: "long", day: "numeric", month: "long" },
|
|
194
194
|
"MMMM YYYY": { month: "long", year: "numeric" }
|
|
195
195
|
};
|
|
196
|
-
function formatDate(date,
|
|
197
|
-
|
|
196
|
+
function formatDate(date, format, locale) {
|
|
197
|
+
const options = typeof format === "string" ? DATE_PRESETS[format] : format;
|
|
198
|
+
return getDateTimeFormat(locale, options).format(toTime(date));
|
|
198
199
|
}
|
|
199
200
|
function formatDateTime(date, locale) {
|
|
200
201
|
return getDateTimeFormat(locale, {
|
|
@@ -249,7 +250,7 @@ function pluralize(count, forms, locale) {
|
|
|
249
250
|
}
|
|
250
251
|
function withLocale(getLocale) {
|
|
251
252
|
return {
|
|
252
|
-
formatDate: (date,
|
|
253
|
+
formatDate: (date, format) => formatDate(date, format, getLocale()),
|
|
253
254
|
formatDateTime: (date) => formatDateTime(date, getLocale()),
|
|
254
255
|
formatCompactTime: (date) => formatCompactTime(date, getLocale()),
|
|
255
256
|
formatDayDivider: (date) => formatDayDivider(date, getLocale()),
|
package/dist/src/i18n/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
// src/i18n/cli.ts
|
|
14
14
|
import { existsSync } from "node:fs";
|
|
15
|
+
import * as nodeModule from "node:module";
|
|
15
16
|
import { dirname, resolve } from "node:path";
|
|
16
17
|
import { pathToFileURL } from "node:url";
|
|
17
18
|
import { z } from "zod";
|
|
@@ -21,10 +22,41 @@ var CONFIG_NAMES = [
|
|
|
21
22
|
"i18n.config.js",
|
|
22
23
|
"i18n.config.mjs"
|
|
23
24
|
];
|
|
25
|
+
var BUNDLER_SUFFIXES = [
|
|
26
|
+
".ts",
|
|
27
|
+
".mts",
|
|
28
|
+
".js",
|
|
29
|
+
".mjs",
|
|
30
|
+
"/index.ts",
|
|
31
|
+
"/index.mts",
|
|
32
|
+
"/index.js",
|
|
33
|
+
"/index.mjs"
|
|
34
|
+
];
|
|
35
|
+
function resolveLikeBundlers() {
|
|
36
|
+
nodeModule.registerHooks?.({
|
|
37
|
+
resolve(specifier, context, nextResolve) {
|
|
38
|
+
try {
|
|
39
|
+
return nextResolve(specifier, context);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const code = error.code;
|
|
42
|
+
const retry = specifier.startsWith(".") && (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_UNSUPPORTED_DIR_IMPORT");
|
|
43
|
+
if (!retry)
|
|
44
|
+
throw error;
|
|
45
|
+
for (const suffix of BUNDLER_SUFFIXES) {
|
|
46
|
+
try {
|
|
47
|
+
return nextResolve(specifier + suffix, context);
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
24
55
|
async function loadConfig(file) {
|
|
25
56
|
const path = file ? resolve(file) : CONFIG_NAMES.map((name) => resolve(name)).find((path) => existsSync(path));
|
|
26
57
|
if (!path || !existsSync(path))
|
|
27
58
|
throw new CliError2(`No i18n config found: pass --config or add ${CONFIG_NAMES[0]}.`);
|
|
59
|
+
resolveLikeBundlers();
|
|
28
60
|
const loaded = await import(pathToFileURL(path).href);
|
|
29
61
|
if (!loaded.default?.catalogs)
|
|
30
62
|
throw new CliError2(`${path} must default-export an i18n config`);
|
|
@@ -80,6 +80,23 @@ type RawHandlerReturnType<T> = T extends (...args: any[]) => infer R ? R : never
|
|
|
80
80
|
type RawCasesReturnType<Cases> = {
|
|
81
81
|
[K in keyof Cases]: RawHandlerReturnType<Cases[K]>;
|
|
82
82
|
}[keyof Cases];
|
|
83
|
+
/**
|
|
84
|
+
* `R` is only inferred from the call's contextual type. Without one it stays
|
|
85
|
+
* `unknown`, and the overload returns the handlers' own return types.
|
|
86
|
+
*/
|
|
87
|
+
type Annotated<R, Inferred> = unknown extends R ? Inferred : R;
|
|
88
|
+
/**
|
|
89
|
+
* TypeScript fixes a handler's return type the first time it checks it, and
|
|
90
|
+
* an object-mode call (`value, key, cases`) is first tried against the
|
|
91
|
+
* plain-value overload with a default (`value, cases, defaultCase`). This type
|
|
92
|
+
* types those cases' handlers by the annotation there too, so they keep their
|
|
93
|
+
* literals. No value satisfies it: `default` would have to be `never`.
|
|
94
|
+
*/
|
|
95
|
+
type ObjectCasesContext<R> = {
|
|
96
|
+
[key: string]: (value: never) => R;
|
|
97
|
+
} & {
|
|
98
|
+
default: never;
|
|
99
|
+
};
|
|
83
100
|
/** Only required, defined handlers guarantee that a case is handled. */
|
|
84
101
|
type HandledCaseKeys<Cases> = {
|
|
85
102
|
[K in keyof Cases]-?: {} extends Pick<Cases, K> ? never : undefined extends Cases[K] ? never : K;
|
|
@@ -113,13 +130,21 @@ export type PartialObjectSwitchCases<T, K extends keyof T, R> = T[K] extends Dis
|
|
|
113
130
|
type NoUnknownCases<Cases, Allowed extends string> = {
|
|
114
131
|
[P in keyof Cases as `${P & (string | number)}` extends Allowed ? never : P]: never;
|
|
115
132
|
};
|
|
133
|
+
export declare function typedSwitch<T extends Discriminant, R, Cases extends {
|
|
134
|
+
[V in T as `${V}`]: (value: V) => R;
|
|
135
|
+
} & NoUnknownCases<Cases, `${T}`>>(value: T, cases: Cases): Annotated<R, RawCasesReturnType<Cases>>;
|
|
136
|
+
export declare function typedSwitch<T extends Discriminant, R, Cases extends {
|
|
137
|
+
[V in T as `${V}`]?: (value: V) => R;
|
|
138
|
+
} & NoUnknownCases<Cases, `${T}`>, DefaultCase extends ((value: Exclude<T, ValuesForKeys<T, HandledCaseKeys<Cases>>>) => R) | ObjectCasesContext<R>>(value: T, cases: Cases, defaultCase: DefaultCase): Annotated<R, RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>>;
|
|
139
|
+
export declare function typedSwitch<T extends object, K extends keyof T, R, Cases extends ObjectCases<T, K, R> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>>(value: T, key: K, cases: Cases): Annotated<R, RawCasesReturnType<Cases>>;
|
|
140
|
+
export declare function typedSwitch<T extends object, K extends keyof T, R, Cases extends PartialObjectCases<T, K, R> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>, DefaultCase extends (value: Exclude<T, Record<K, ValuesForKeys<T[K], HandledCaseKeys<Cases>>>>) => R>(value: T, key: K, cases: Cases, defaultCase: DefaultCase): Annotated<R, RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>>;
|
|
116
141
|
export declare function typedSwitch<T extends Discriminant, Cases extends {
|
|
117
142
|
[V in T as `${V}`]: (value: V) => unknown;
|
|
118
143
|
} & NoUnknownCases<Cases, `${T}`>>(value: T, cases: Cases): RawCasesReturnType<Cases>;
|
|
144
|
+
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>;
|
|
119
145
|
export declare function typedSwitch<T extends Discriminant, Cases extends {
|
|
120
146
|
[V in T as `${V}`]?: (value: V) => unknown;
|
|
121
147
|
} & 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
148
|
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
149
|
/**
|
|
125
150
|
* Builds cases that share one handler, for spreading into `typedSwitch` cases.
|
package/package.json
CHANGED
package/src/format/README.md
CHANGED
|
@@ -87,6 +87,13 @@ formatDateTime(date, 'en-GB') // '5 Mar 2026, 07:04' (newer ICU: '5 Mar 2026 at
|
|
|
87
87
|
Presets: `'DD/MM/YYYY'`, `'DD/MM/YYYY HH:mm'`, `'HH:mm'`, `'ddd'`, `'D MMM'`,
|
|
88
88
|
`'D MMM YYYY'`, `'dddd, MMMM D'`, `'MMMM YYYY'`.
|
|
89
89
|
|
|
90
|
+
For any other layout, pass `Intl.DateTimeFormat` options instead of a preset:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
formatDate(date, { weekday: 'long', day: 'numeric', month: 'long' }, 'en-GB') // 'Thursday 5 March'
|
|
94
|
+
formatDate(date, { dateStyle: 'short', timeZone: 'Europe/Copenhagen' }, 'da-DK') // '05.03.2026'
|
|
95
|
+
```
|
|
96
|
+
|
|
90
97
|
`formatCompactTime(date, locale, { now? })` is an activity timestamp for list
|
|
91
98
|
rows: the time today, "yesterday", else the date (with the year when it is not
|
|
92
99
|
this year). `formatDayDivider(date, locale, { now? })` labels a chat timeline:
|
package/src/format/index.ts
CHANGED
|
@@ -401,13 +401,17 @@ const DATE_PRESETS = {
|
|
|
401
401
|
*/
|
|
402
402
|
export type DatePreset = keyof typeof DATE_PRESETS
|
|
403
403
|
|
|
404
|
-
/**
|
|
404
|
+
/**
|
|
405
|
+
* A date in one of the `DatePreset` layouts, or with any
|
|
406
|
+
* `Intl.DateTimeFormat` options, in local time unless they set `timeZone`.
|
|
407
|
+
*/
|
|
405
408
|
export function formatDate(
|
|
406
409
|
date: DateInput,
|
|
407
|
-
|
|
410
|
+
format: DatePreset | Intl.DateTimeFormatOptions,
|
|
408
411
|
locale: string
|
|
409
412
|
): string {
|
|
410
|
-
|
|
413
|
+
const options = typeof format === 'string' ? DATE_PRESETS[format] : format
|
|
414
|
+
return getDateTimeFormat(locale, options).format(toTime(date))
|
|
411
415
|
}
|
|
412
416
|
|
|
413
417
|
/** Medium date and short time: "5 Mar 2026, 14:30" (en-GB). */
|
|
@@ -519,8 +523,10 @@ export function pluralize(
|
|
|
519
523
|
*/
|
|
520
524
|
export function withLocale(getLocale: () => string) {
|
|
521
525
|
return {
|
|
522
|
-
formatDate: (
|
|
523
|
-
|
|
526
|
+
formatDate: (
|
|
527
|
+
date: DateInput,
|
|
528
|
+
format: DatePreset | Intl.DateTimeFormatOptions
|
|
529
|
+
) => formatDate(date, format, getLocale()),
|
|
524
530
|
formatDateTime: (date: DateInput) => formatDateTime(date, getLocale()),
|
|
525
531
|
formatCompactTime: (date: DateInput) =>
|
|
526
532
|
formatCompactTime(date, getLocale()),
|
package/src/i18n/README.md
CHANGED
|
@@ -203,7 +203,9 @@ Both read `i18n.config.ts` (or `.mts`, `.js`, `.mjs`, or `--config <file>`)
|
|
|
203
203
|
from the current directory; paths in it are relative to the config file.
|
|
204
204
|
`defineI18nConfig` rejects a `baseLocale`, `disjointNamespaces` catalog or
|
|
205
205
|
`identical.allowKeys` locale the config does not declare. A
|
|
206
|
-
`.ts` config needs Bun
|
|
206
|
+
`.ts` config needs Bun or Node 22.18+. Its relative imports may leave out the
|
|
207
|
+
extension or `/index`, as TypeScript's bundler resolution allows, so
|
|
208
|
+
`npx hanzio-i18n check` works without `bun --bun`. The bin is built
|
|
207
209
|
on [`hanzio/cli`](../cli/README.md), so it needs `zod` installed; `--help`
|
|
208
210
|
lists its options and it exits 1 on catalog problems, 2 on bad arguments.
|
|
209
211
|
|
package/src/i18n/cli.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// a Node version that strips types.
|
|
6
6
|
|
|
7
7
|
import { existsSync } from 'node:fs'
|
|
8
|
+
import * as nodeModule from 'node:module'
|
|
8
9
|
import { dirname, resolve } from 'node:path'
|
|
9
10
|
import { pathToFileURL } from 'node:url'
|
|
10
11
|
import { z } from 'zod'
|
|
@@ -25,6 +26,44 @@ const CONFIG_NAMES = [
|
|
|
25
26
|
|
|
26
27
|
type Loaded = { config: I18nConfig; root: string }
|
|
27
28
|
|
|
29
|
+
// Node resolves a config's imports like a browser: `./src/locales` fails
|
|
30
|
+
// because it names neither a file nor an extension. Bun resolves it like a
|
|
31
|
+
// bundler. These are the suffixes TypeScript's bundler resolution tries.
|
|
32
|
+
const BUNDLER_SUFFIXES = [
|
|
33
|
+
'.ts',
|
|
34
|
+
'.mts',
|
|
35
|
+
'.js',
|
|
36
|
+
'.mjs',
|
|
37
|
+
'/index.ts',
|
|
38
|
+
'/index.mts',
|
|
39
|
+
'/index.js',
|
|
40
|
+
'/index.mjs'
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
function resolveLikeBundlers() {
|
|
44
|
+
// Missing under Bun, which needs no help, and under Node before 22.15.
|
|
45
|
+
nodeModule.registerHooks?.({
|
|
46
|
+
resolve(specifier, context, nextResolve) {
|
|
47
|
+
try {
|
|
48
|
+
return nextResolve(specifier, context)
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const code = (error as { code?: string }).code
|
|
51
|
+
const retry =
|
|
52
|
+
specifier.startsWith('.') &&
|
|
53
|
+
(code === 'ERR_MODULE_NOT_FOUND' ||
|
|
54
|
+
code === 'ERR_UNSUPPORTED_DIR_IMPORT')
|
|
55
|
+
if (!retry) throw error
|
|
56
|
+
for (const suffix of BUNDLER_SUFFIXES) {
|
|
57
|
+
try {
|
|
58
|
+
return nextResolve(specifier + suffix, context)
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
throw error
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
28
67
|
async function loadConfig(file: string | undefined): Promise<Loaded> {
|
|
29
68
|
const path = file
|
|
30
69
|
? resolve(file)
|
|
@@ -33,6 +72,7 @@ async function loadConfig(file: string | undefined): Promise<Loaded> {
|
|
|
33
72
|
throw new CliError(
|
|
34
73
|
`No i18n config found: pass --config or add ${CONFIG_NAMES[0]}.`
|
|
35
74
|
)
|
|
75
|
+
resolveLikeBundlers()
|
|
36
76
|
const loaded = (await import(pathToFileURL(path).href)) as {
|
|
37
77
|
default?: I18nConfig
|
|
38
78
|
}
|
|
@@ -246,21 +246,32 @@ const result = typedSwitch(status, {
|
|
|
246
246
|
|
|
247
247
|
### Enforcing a Return Type
|
|
248
248
|
|
|
249
|
-
Annotate the result
|
|
250
|
-
checking still applies
|
|
249
|
+
Annotate the result, or switch in a function with a declared return type.
|
|
250
|
+
Every handler is checked against it, exhaustiveness checking still applies,
|
|
251
|
+
and literals keep their types instead of widening to `string`:
|
|
251
252
|
|
|
252
253
|
```typescript
|
|
253
|
-
|
|
254
|
-
id: string
|
|
255
|
-
}
|
|
254
|
+
type Severity = 'success' | 'info' | 'warn' | 'danger'
|
|
256
255
|
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
256
|
+
const severity: Severity = typedSwitch(quality, {
|
|
257
|
+
excellent: () => 'success',
|
|
258
|
+
good: () => 'info',
|
|
259
|
+
fair: () => 'warn',
|
|
260
|
+
poor: () => 'danger',
|
|
261
261
|
})
|
|
262
|
+
|
|
263
|
+
function orderBy(sort: Sort): Prisma.OrderOrderByWithRelationInput[] {
|
|
264
|
+
return typedSwitch(sort, {
|
|
265
|
+
name: () => [{ name: 'asc' }],
|
|
266
|
+
newest: () => [{ createdAt: 'desc' }],
|
|
267
|
+
})
|
|
268
|
+
}
|
|
262
269
|
```
|
|
263
270
|
|
|
271
|
+
Without an annotation, or inside a generic call such as `useState(typedSwitch(...))`,
|
|
272
|
+
the result is the union of the handlers' widened return types. In Vue, annotate
|
|
273
|
+
the getter: `computed((): Severity => typedSwitch(...))`.
|
|
274
|
+
|
|
264
275
|
## Error Handling
|
|
265
276
|
|
|
266
277
|
When a case is unhandled at runtime (e.g., due to unsafe casts), `typedSwitch` throws a descriptive error:
|
package/src/typedswitch/index.ts
CHANGED
|
@@ -88,6 +88,23 @@ type RawCasesReturnType<Cases> = {
|
|
|
88
88
|
[K in keyof Cases]: RawHandlerReturnType<Cases[K]>
|
|
89
89
|
}[keyof Cases]
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* `R` is only inferred from the call's contextual type. Without one it stays
|
|
93
|
+
* `unknown`, and the overload returns the handlers' own return types.
|
|
94
|
+
*/
|
|
95
|
+
type Annotated<R, Inferred> = unknown extends R ? Inferred : R
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* TypeScript fixes a handler's return type the first time it checks it, and
|
|
99
|
+
* an object-mode call (`value, key, cases`) is first tried against the
|
|
100
|
+
* plain-value overload with a default (`value, cases, defaultCase`). This type
|
|
101
|
+
* types those cases' handlers by the annotation there too, so they keep their
|
|
102
|
+
* literals. No value satisfies it: `default` would have to be `never`.
|
|
103
|
+
*/
|
|
104
|
+
type ObjectCasesContext<R> = { [key: string]: (value: never) => R } & {
|
|
105
|
+
default: never
|
|
106
|
+
}
|
|
107
|
+
|
|
91
108
|
/** Only required, defined handlers guarantee that a case is handled. */
|
|
92
109
|
type HandledCaseKeys<Cases> = {
|
|
93
110
|
// biome-ignore lint/complexity/noBannedTypes: Empty-object assignability detects optional keys and index signatures.
|
|
@@ -173,6 +190,65 @@ type CasesRecord = Record<string, Handler | undefined>
|
|
|
173
190
|
// Function overloads
|
|
174
191
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
175
192
|
|
|
193
|
+
// Each mode has two overloads. The first four take the result type `R` from
|
|
194
|
+
// the call's contextual type, such as an annotated variable or a declared
|
|
195
|
+
// return type, so handlers that return literals ('info', [{ name: 'asc' }])
|
|
196
|
+
// keep them. When that does not type-check, for example inside a generic call
|
|
197
|
+
// like useState(typedSwitch(...)), the last four union the handlers' types.
|
|
198
|
+
|
|
199
|
+
// Plain value input - all cases required, typed by the annotation
|
|
200
|
+
export function typedSwitch<
|
|
201
|
+
T extends Discriminant,
|
|
202
|
+
R,
|
|
203
|
+
Cases extends { [V in T as `${V}`]: (value: V) => R } & NoUnknownCases<
|
|
204
|
+
Cases,
|
|
205
|
+
`${T}`
|
|
206
|
+
>
|
|
207
|
+
>(value: T, cases: Cases): Annotated<R, RawCasesReturnType<Cases>>
|
|
208
|
+
|
|
209
|
+
// Plain value input - partial cases with default, typed by the annotation.
|
|
210
|
+
// It comes before the object overloads, which also take three arguments.
|
|
211
|
+
export function typedSwitch<
|
|
212
|
+
T extends Discriminant,
|
|
213
|
+
R,
|
|
214
|
+
Cases extends {
|
|
215
|
+
[V in T as `${V}`]?: (value: V) => R
|
|
216
|
+
} & NoUnknownCases<Cases, `${T}`>,
|
|
217
|
+
DefaultCase extends
|
|
218
|
+
| ((value: Exclude<T, ValuesForKeys<T, HandledCaseKeys<Cases>>>) => R)
|
|
219
|
+
| ObjectCasesContext<R>
|
|
220
|
+
>(
|
|
221
|
+
value: T,
|
|
222
|
+
cases: Cases,
|
|
223
|
+
defaultCase: DefaultCase
|
|
224
|
+
): Annotated<R, RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>>
|
|
225
|
+
|
|
226
|
+
// Object input with key - all cases required, typed by the annotation
|
|
227
|
+
export function typedSwitch<
|
|
228
|
+
T extends object,
|
|
229
|
+
K extends keyof T,
|
|
230
|
+
R,
|
|
231
|
+
Cases extends ObjectCases<T, K, R> &
|
|
232
|
+
NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>
|
|
233
|
+
>(value: T, key: K, cases: Cases): Annotated<R, RawCasesReturnType<Cases>>
|
|
234
|
+
|
|
235
|
+
// Object input with key - partial cases with default, typed by the annotation
|
|
236
|
+
export function typedSwitch<
|
|
237
|
+
T extends object,
|
|
238
|
+
K extends keyof T,
|
|
239
|
+
R,
|
|
240
|
+
Cases extends PartialObjectCases<T, K, R> &
|
|
241
|
+
NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>,
|
|
242
|
+
DefaultCase extends (
|
|
243
|
+
value: Exclude<T, Record<K, ValuesForKeys<T[K], HandledCaseKeys<Cases>>>>
|
|
244
|
+
) => R
|
|
245
|
+
>(
|
|
246
|
+
value: T,
|
|
247
|
+
key: K,
|
|
248
|
+
cases: Cases,
|
|
249
|
+
defaultCase: DefaultCase
|
|
250
|
+
): Annotated<R, RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>>
|
|
251
|
+
|
|
176
252
|
// Plain value input - all cases required (no default)
|
|
177
253
|
export function typedSwitch<
|
|
178
254
|
T extends Discriminant,
|
|
@@ -182,6 +258,14 @@ export function typedSwitch<
|
|
|
182
258
|
>
|
|
183
259
|
>(value: T, cases: Cases): RawCasesReturnType<Cases>
|
|
184
260
|
|
|
261
|
+
// Object input with key - all cases required (no default)
|
|
262
|
+
export function typedSwitch<
|
|
263
|
+
T extends object,
|
|
264
|
+
K extends keyof T,
|
|
265
|
+
Cases extends ObjectCases<T, K, unknown> &
|
|
266
|
+
NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>
|
|
267
|
+
>(value: T, key: K, cases: Cases): RawCasesReturnType<Cases>
|
|
268
|
+
|
|
185
269
|
// Plain value input - partial cases allowed (with default)
|
|
186
270
|
// The default handler only receives the cases not covered by `cases`.
|
|
187
271
|
export function typedSwitch<
|
|
@@ -198,14 +282,6 @@ export function typedSwitch<
|
|
|
198
282
|
defaultCase: DefaultCase
|
|
199
283
|
): RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>
|
|
200
284
|
|
|
201
|
-
// Object input with key - all cases required (no default)
|
|
202
|
-
export function typedSwitch<
|
|
203
|
-
T extends object,
|
|
204
|
-
K extends keyof T,
|
|
205
|
-
Cases extends ObjectCases<T, K, unknown> &
|
|
206
|
-
NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>
|
|
207
|
-
>(value: T, key: K, cases: Cases): RawCasesReturnType<Cases>
|
|
208
|
-
|
|
209
285
|
// Object input with key - partial cases allowed (with default)
|
|
210
286
|
// The default handler only receives the union members not covered by `cases`.
|
|
211
287
|
export function typedSwitch<
|