unacy 0.8.1 → 0.8.2
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/dist/converters.d.ts +106 -17
- package/dist/converters.d.ts.map +1 -1
- package/dist/errors.d.ts +132 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +132 -5
- package/dist/errors.js.map +1 -1
- package/dist/formatters.d.ts +96 -32
- package/dist/formatters.d.ts.map +1 -1
- package/dist/registry.d.ts +151 -21
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +52 -15
- package/dist/registry.js.map +1 -1
- package/dist/types.d.ts +275 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/graph.d.ts +72 -10
- package/dist/utils/graph.d.ts.map +1 -1
- package/dist/utils/graph.js +72 -10
- package/dist/utils/graph.js.map +1 -1
- package/dist/utils/validation.d.ts +73 -7
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +73 -7
- package/dist/utils/validation.js.map +1 -1
- package/package.json +7 -7
package/dist/types.d.ts
CHANGED
|
@@ -9,7 +9,21 @@ import type { GetTagMetadata, Simplify, Tagged, UnwrapTagged } from 'type-fest';
|
|
|
9
9
|
* giving us the `Tag` constraint without importing the internal `Tag` type.
|
|
10
10
|
*/
|
|
11
11
|
type AnyTaggedUnit = Tagged<unknown, typeof UNITS, any>;
|
|
12
|
+
/**
|
|
13
|
+
* Internal phantom-type brand key used to tag values with unit metadata.
|
|
14
|
+
* Consumers should not use this symbol directly — use `WithUnits<T, M>` instead.
|
|
15
|
+
*
|
|
16
|
+
* @category Branding
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
12
19
|
export declare const UNITS: unique symbol;
|
|
20
|
+
/**
|
|
21
|
+
* Reserved symbol for future definition attachment on unit metadata.
|
|
22
|
+
* Not used in the current public API.
|
|
23
|
+
*
|
|
24
|
+
* @category Branding
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
13
27
|
export declare const DEFINITION: unique symbol;
|
|
14
28
|
/**
|
|
15
29
|
* Resolve a schema/constructor/enum type to its runtime value type.
|
|
@@ -19,6 +33,8 @@ export declare const DEFINITION: unique symbol;
|
|
|
19
33
|
* - **Record schemas** → `InferFromRecordSchema` (`{ x: 'number' }` → `{ x: number }`)
|
|
20
34
|
* - **Enum objects** → `T[keyof T]` (enum value type)
|
|
21
35
|
* - **Primitives** → passed through unchanged
|
|
36
|
+
*
|
|
37
|
+
* @category Type Utilities
|
|
22
38
|
*/
|
|
23
39
|
export type ResolveValueType<T> = T extends readonly string[] ? InferFromTupleSchema<T> : T extends ClassType ? InstanceType<T> : T extends RecordSchema ? T[keyof T] extends keyof PrimitiveTypeMap | RecordSchema ? InferFromRecordSchema<T> : T[keyof T] : T extends EnumType ? T[keyof T] : T;
|
|
24
40
|
/**
|
|
@@ -37,6 +53,18 @@ export type ResolveValueType<T> = T extends readonly string[] ? InferFromTupleSc
|
|
|
37
53
|
* type Celsius = WithTypedUnits<typeof CelsiusMeta>;
|
|
38
54
|
* // Celsius = Tagged<number, UNITS, typeof CelsiusMeta>
|
|
39
55
|
* ```
|
|
56
|
+
*
|
|
57
|
+
* @useWhen You have a metadata object with an explicit `type` field and want
|
|
58
|
+
* the branded type to reflect the correct base type automatically.
|
|
59
|
+
*
|
|
60
|
+
* @avoidWhen You only have a `BaseMetadata` object without a `type` field — use
|
|
61
|
+
* `WithUnits<T, M>` directly, specifying the base type explicitly.
|
|
62
|
+
*
|
|
63
|
+
* @pitfalls
|
|
64
|
+
* NEVER pass `any` as the type parameter — `WithTypedUnits<any>` widens to
|
|
65
|
+
* `WithUnits<any, any>`, bypassing all type checks.
|
|
66
|
+
*
|
|
67
|
+
* @category Branding
|
|
40
68
|
*/
|
|
41
69
|
export type WithTypedUnits<M extends TypedMetadata<any>> = unknown extends M ? WithUnits<any, any> : M extends {
|
|
42
70
|
name: string;
|
|
@@ -45,29 +73,89 @@ export type WithTypedUnits<M extends TypedMetadata<any>> = unknown extends M ? W
|
|
|
45
73
|
/**
|
|
46
74
|
* Brand a value with a unit identifier for compile-time unit safety.
|
|
47
75
|
*
|
|
76
|
+
* The branding is purely a compile-time phantom — at runtime, values are plain
|
|
77
|
+
* `T` (number, string, etc.) with zero overhead. Branded values extend their
|
|
78
|
+
* base type, so they are assignable to plain `T` but not vice versa.
|
|
79
|
+
*
|
|
48
80
|
* @template T - Base type (e.g., number, bigint, record, tuple, class instance)
|
|
49
81
|
* @template M - Metadata type (must extend BaseMetadata with required name property)
|
|
50
82
|
*
|
|
51
83
|
* @example
|
|
52
84
|
* ```typescript
|
|
53
|
-
* const
|
|
54
|
-
* type Celsius = WithUnits<number, typeof
|
|
85
|
+
* const CelsiusMeta = { name: 'Celsius' as const, symbol: '°C' } satisfies BaseMetadata;
|
|
86
|
+
* type Celsius = WithUnits<number, typeof CelsiusMeta>;
|
|
55
87
|
* const temp: Celsius = 25 as Celsius;
|
|
88
|
+
*
|
|
89
|
+
* // Celsius is assignable to number, but number is not assignable to Celsius
|
|
90
|
+
* const raw: number = temp; // OK
|
|
91
|
+
* const invalid: Celsius = raw; // TS error
|
|
56
92
|
* ```
|
|
93
|
+
*
|
|
94
|
+
* @useWhen You need a branded unit type and your metadata does not have a `type`
|
|
95
|
+
* field (or you want to specify the base type explicitly).
|
|
96
|
+
*
|
|
97
|
+
* @avoidWhen Your metadata already carries a `type` field — prefer `WithTypedUnits<M>`
|
|
98
|
+
* to let the compiler infer the correct base type automatically.
|
|
99
|
+
*
|
|
100
|
+
* @pitfalls
|
|
101
|
+
* NEVER use `as` to cast a plain `number` to `WithUnits<number, M>` in
|
|
102
|
+
* application code without validating the value first — the cast bypasses
|
|
103
|
+
* every compile-time guarantee that `WithUnits` provides.
|
|
104
|
+
*
|
|
105
|
+
* NEVER assign a value of `WithUnits<number, CelsiusMetadata>` to a variable
|
|
106
|
+
* typed `WithUnits<number, FahrenheitMetadata>` via `as` — the phantom types
|
|
107
|
+
* become meaningless if the brand is forged at assignment sites.
|
|
108
|
+
*
|
|
109
|
+
* @category Branding
|
|
110
|
+
* @see WithTypedUnits
|
|
57
111
|
*/
|
|
58
112
|
export type WithUnits<T, M extends BaseMetadata = BaseMetadata> = Tagged<T, typeof UNITS, M>;
|
|
59
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Primitive JavaScript types that can be used as unit base types.
|
|
115
|
+
*
|
|
116
|
+
* @remarks
|
|
117
|
+
* Covers the four scalar primitive types that unacy supports as direct base
|
|
118
|
+
* types for `WithUnits<T, M>`. Non-primitive base types (classes, enums,
|
|
119
|
+
* records, tuples) are handled via `SupportedType`.
|
|
120
|
+
*
|
|
121
|
+
* @category Types
|
|
122
|
+
*/
|
|
60
123
|
export type PrimitiveType = number | string | boolean | bigint;
|
|
61
124
|
/**
|
|
62
125
|
* A TypeScript enum object at runtime — an object whose values are all
|
|
63
126
|
* strings (string enum) or all numbers (numeric enum).
|
|
64
127
|
* Mixed enums (both string and number values) are rejected at validation.
|
|
128
|
+
*
|
|
129
|
+
* @remarks
|
|
130
|
+
* TypeScript numeric enums produce reverse-mapped keys at runtime
|
|
131
|
+
* (e.g., `{ DEBUG: 0, 0: 'DEBUG' }`). `validateEnum` filters those out
|
|
132
|
+
* automatically — you do not need to handle them yourself.
|
|
133
|
+
*
|
|
134
|
+
* @pitfalls
|
|
135
|
+
* NEVER use an empty object (`{}`) as an enum type — `validateEnum` rejects
|
|
136
|
+
* it, and the registry will throw at registration time.
|
|
137
|
+
*
|
|
138
|
+
* NEVER mix numeric and string values in a single enum — unacy rejects mixed
|
|
139
|
+
* enums at validation time because the value type would be ambiguous.
|
|
140
|
+
*
|
|
141
|
+
* @category Types
|
|
65
142
|
*/
|
|
66
143
|
export type EnumType = Record<string, string | number>;
|
|
67
144
|
/**
|
|
68
145
|
* A class constructor (including abstract classes) that can serve as a
|
|
69
146
|
* unit's type identity. At runtime, the constructor itself is stored
|
|
70
147
|
* in the metadata `type` field.
|
|
148
|
+
*
|
|
149
|
+
* @remarks
|
|
150
|
+
* When the registry detects a class-typed unit (via `isClassMetadata`), the
|
|
151
|
+
* unit accessor's brand function calls `new Constructor(...args)` so that
|
|
152
|
+
* `registry.MyUnit(arg1, arg2)` returns a properly constructed instance.
|
|
153
|
+
*
|
|
154
|
+
* @pitfalls
|
|
155
|
+
* NEVER pass an arrow function or a bound function as a `ClassType` — they
|
|
156
|
+
* lack a `prototype` property and will fail `validateClass` at registration.
|
|
157
|
+
*
|
|
158
|
+
* @category Types
|
|
71
159
|
*/
|
|
72
160
|
export type ClassType = abstract new (...args: any[]) => any;
|
|
73
161
|
/** A record schema value — either a primitive type name or a nested schema. */
|
|
@@ -80,7 +168,17 @@ export type RecordSchemaValue = string | RecordSchema;
|
|
|
80
168
|
* @example
|
|
81
169
|
* ```typescript
|
|
82
170
|
* const PointSchema = { x: 'number', y: 'number' } satisfies RecordSchema;
|
|
171
|
+
* const NestedSchema = { pos: { x: 'number', y: 'number' }, label: 'string' } satisfies RecordSchema;
|
|
83
172
|
* ```
|
|
173
|
+
*
|
|
174
|
+
* @pitfalls
|
|
175
|
+
* NEVER use a circular schema object — `validateRecordSchema` detects circular
|
|
176
|
+
* references and throws, so the registry will refuse to register such a unit.
|
|
177
|
+
*
|
|
178
|
+
* NEVER use non-primitive type names as leaf values (e.g., `'Date'`) — only
|
|
179
|
+
* `'number'`, `'string'`, `'boolean'`, and `'bigint'` are accepted.
|
|
180
|
+
*
|
|
181
|
+
* @category Types
|
|
84
182
|
*/
|
|
85
183
|
export type RecordSchema = {
|
|
86
184
|
[key: string]: RecordSchemaValue;
|
|
@@ -92,12 +190,33 @@ export type RecordSchema = {
|
|
|
92
190
|
* @example
|
|
93
191
|
* ```typescript
|
|
94
192
|
* const RGBSchema = ['number', 'number', 'number'] as const satisfies TupleSchema;
|
|
193
|
+
* const FlexSchema = ['string', 'number?', '...boolean'] as const satisfies TupleSchema;
|
|
95
194
|
* ```
|
|
195
|
+
*
|
|
196
|
+
* @remarks
|
|
197
|
+
* When the registry detects a tuple-typed unit (via `isTupleMetadata`), the
|
|
198
|
+
* brand function collects spread arguments into an array. Pass each tuple
|
|
199
|
+
* member as a separate argument: `registry.RGB(255, 128, 0)` instead of
|
|
200
|
+
* `registry.RGB([255, 128, 0])`.
|
|
201
|
+
*
|
|
202
|
+
* @pitfalls
|
|
203
|
+
* NEVER declare a rest element (`'...type'`) in a non-terminal position —
|
|
204
|
+
* `validateTupleSchema` accepts it syntactically, but `InferFromTupleSchema`
|
|
205
|
+
* only handles rest at the last position and returns `never` otherwise.
|
|
206
|
+
*
|
|
207
|
+
* @category Types
|
|
96
208
|
*/
|
|
97
209
|
export type TupleSchema = readonly string[];
|
|
98
210
|
/**
|
|
99
211
|
* Union of all types that can be used as a unit's base type.
|
|
100
212
|
* Includes primitives and non-primitive categories (enum, class, record, tuple).
|
|
213
|
+
*
|
|
214
|
+
* @remarks
|
|
215
|
+
* `SupportedType` is the constraint on the `T` parameter of `WithUnits<T, M>`
|
|
216
|
+
* and on the `type` field of `TypedMetadata<T>`. The registry uses
|
|
217
|
+
* `detectMetadataKind` to dispatch on which member of the union is present.
|
|
218
|
+
*
|
|
219
|
+
* @category Types
|
|
101
220
|
*/
|
|
102
221
|
export type SupportedType = PrimitiveType | EnumType | ClassType | RecordSchema | TupleSchema;
|
|
103
222
|
export type PrimitiveTypeMap = {
|
|
@@ -106,6 +225,11 @@ export type PrimitiveTypeMap = {
|
|
|
106
225
|
boolean: boolean;
|
|
107
226
|
bigint: bigint;
|
|
108
227
|
};
|
|
228
|
+
/**
|
|
229
|
+
* Map a primitive TypeScript type to its corresponding type name string.
|
|
230
|
+
* For example, `number` → `'number'`, `string` → `'string'`, `boolean` → `'boolean'`, `bigint` → `'bigint'`.
|
|
231
|
+
* Returns `never` for non-primitive types.
|
|
232
|
+
*/
|
|
109
233
|
export type ToPrimitiveTypeName<T> = T extends PrimitiveTypeMap[infer U extends keyof PrimitiveTypeMap] ? U : never;
|
|
110
234
|
export type OptionalWithUnits<T, M extends BaseMetadata = BaseMetadata> = T | WithUnits<T, M>;
|
|
111
235
|
export type Unwrap<T> = T extends AnyTaggedUnit ? UnwrapTagged<T> : T;
|
|
@@ -123,18 +247,71 @@ export type Unwrap<T> = T extends AnyTaggedUnit ? UnwrapTagged<T> : T;
|
|
|
123
247
|
export type InferCallableArgs<From> = From extends AnyTaggedUnit ? GetTagMetadata<From, typeof UNITS> extends {
|
|
124
248
|
type: infer TypeField;
|
|
125
249
|
} ? TypeField extends readonly string[] ? InferFromTupleSchema<TypeField> : TypeField extends ClassType ? ConstructorParameters<TypeField> : TypeField extends RecordSchema ? TypeField[keyof TypeField] extends keyof PrimitiveTypeMap | RecordSchema ? [InferFromRecordSchema<TypeField>] : [TypeField[keyof TypeField]] : TypeField extends EnumType ? [TypeField[keyof TypeField]] : [Unwrap<From>] : [Unwrap<From>] : [Unwrap<From>];
|
|
250
|
+
/**
|
|
251
|
+
* Relax a branded unit type to accept either the branded form or its raw unwrapped value.
|
|
252
|
+
* Useful for APIs that should accept both `WithUnits<T, M>` and plain `T` interchangeably.
|
|
253
|
+
*
|
|
254
|
+
* @remarks
|
|
255
|
+
* Use `Relax<T>` in converter or utility signatures where callers may hold a
|
|
256
|
+
* plain value (e.g., coming from a JSON payload) alongside properly branded
|
|
257
|
+
* values. The type still communicates intent while remaining ergonomic.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```typescript
|
|
261
|
+
* function display(temp: Relax<Celsius>): string {
|
|
262
|
+
* return `${temp}°C`; // accepts both: Celsius brand or plain number
|
|
263
|
+
* }
|
|
264
|
+
* display(25); // OK — plain number
|
|
265
|
+
* display(25 as Celsius); // OK — branded
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
268
|
+
* @useWhen Writing utility functions that wrap or log branded values and
|
|
269
|
+
* should remain usable before the caller has set up a full registry.
|
|
270
|
+
*
|
|
271
|
+
* @avoidWhen Writing converter functions that must enforce the source brand —
|
|
272
|
+
* use the explicit branded type (`Celsius`) to preserve the safety guarantee.
|
|
273
|
+
*
|
|
274
|
+
* @category Branding
|
|
275
|
+
*/
|
|
126
276
|
export type Relax<T> = T | Unwrap<T>;
|
|
127
277
|
/**
|
|
128
278
|
* Brand a value with a format identifier for compile-time format safety.
|
|
129
279
|
*
|
|
280
|
+
* Analogous to `WithUnits<T, M>` but for format tags rather than unit
|
|
281
|
+
* metadata. Ensures a formatted string, `Date`, or number cannot be
|
|
282
|
+
* passed to a formatter/parser expecting a different format.
|
|
283
|
+
*
|
|
130
284
|
* @template T - Base type (e.g., Date, number, string)
|
|
131
|
-
* @template F - Format identifier (e.g., 'ISO8601'
|
|
285
|
+
* @template F - Format identifier string literal (e.g., `'ISO8601'`, `'UnixTimestamp'`)
|
|
132
286
|
*
|
|
133
287
|
* @example
|
|
134
288
|
* ```typescript
|
|
135
|
-
* type
|
|
136
|
-
*
|
|
289
|
+
* type ISO8601Date = WithFormat<Date, 'ISO8601'>;
|
|
290
|
+
* type UnixTimestamp = WithFormat<number, 'UnixTimestamp'>;
|
|
291
|
+
*
|
|
292
|
+
* const parseISO: Parser<ISO8601Date> = (s) => new Date(s) as ISO8601Date;
|
|
293
|
+
* const formatISO: Formatter<ISO8601Date> = (d) => d.toISOString();
|
|
137
294
|
* ```
|
|
295
|
+
*
|
|
296
|
+
* @useWhen You need compile-time guarantees that a value has been validated
|
|
297
|
+
* and tagged with a specific serialisation format before it can be formatted
|
|
298
|
+
* or passed into format-aware APIs.
|
|
299
|
+
*
|
|
300
|
+
* @avoidWhen The value is plain and needs no format guarantee — use the bare
|
|
301
|
+
* base type (`Date`, `number`, `string`) directly.
|
|
302
|
+
*
|
|
303
|
+
* @pitfalls
|
|
304
|
+
* NEVER cast an unvalidated value to `WithFormat<T, F>` — use a `Parser`
|
|
305
|
+
* that validates the input string first and only then tags the result.
|
|
306
|
+
*
|
|
307
|
+
* NEVER assume that round-tripping through `format` then `parse` is
|
|
308
|
+
* lossless for all inputs — floating-point serialisation and timezone
|
|
309
|
+
* handling can introduce discrepancies.
|
|
310
|
+
*
|
|
311
|
+
* @category Branding
|
|
312
|
+
* @see Formatter
|
|
313
|
+
* @see Parser
|
|
314
|
+
* @see FormatterParser
|
|
138
315
|
*/
|
|
139
316
|
export type WithFormat<T, F extends string> = Tagged<T, typeof UNITS, F>;
|
|
140
317
|
export type UnitsOf<T> = T extends AnyTaggedUnit ? GetTagMetadata<T, typeof UNITS> : never;
|
|
@@ -149,21 +326,45 @@ export type MetadataOf<T> = T extends AnyTaggedUnit ? GetTagMetadata<T, typeof U
|
|
|
149
326
|
* Base metadata type that all unit metadata must extend.
|
|
150
327
|
* Requires a `name` property and allows arbitrary additional properties.
|
|
151
328
|
*
|
|
329
|
+
* @remarks
|
|
330
|
+
* `name` is the registry key — it must be a string literal (`as const`) so
|
|
331
|
+
* that the type-level accessor map (`UnitMap`) can index by it. At runtime it
|
|
332
|
+
* is also the adjacency-map key used for BFS path finding, so names must be
|
|
333
|
+
* unique within a registry.
|
|
334
|
+
*
|
|
152
335
|
* @example
|
|
153
336
|
* ```typescript
|
|
154
|
-
* const
|
|
155
|
-
* name: 'Celsius' as const,
|
|
337
|
+
* const CelsiusMeta = {
|
|
338
|
+
* name: 'Celsius' as const, // must be a literal
|
|
156
339
|
* symbol: '°C',
|
|
157
340
|
* description: 'Temperature in Celsius'
|
|
158
341
|
* } satisfies BaseMetadata;
|
|
159
342
|
* ```
|
|
343
|
+
*
|
|
344
|
+
* @useWhen Defining metadata for a unit that only needs a name and optional
|
|
345
|
+
* display fields (symbol, abbreviation, description). For units where the
|
|
346
|
+
* base type matters in conversions, use `TypedMetadata<T>`.
|
|
347
|
+
*
|
|
348
|
+
* @pitfalls
|
|
349
|
+
* NEVER use a non-literal string for `name` (e.g., `name: string` instead of
|
|
350
|
+
* `name: 'Celsius' as const`) — the registry key becomes `string` and the
|
|
351
|
+
* compile-time accessor `registry.Celsius.to.Fahrenheit` stops resolving.
|
|
352
|
+
*
|
|
353
|
+
* NEVER reuse the same `name` value across two different unit objects in the
|
|
354
|
+
* same registry — the second registration silently overwrites the first.
|
|
355
|
+
*
|
|
356
|
+
* @config
|
|
357
|
+
* Additional properties (e.g., `symbol`, `abbreviation`, `description`) are
|
|
358
|
+
* accessible as `registry.<UnitName>.<property>` after registration.
|
|
359
|
+
*
|
|
360
|
+
* @category Metadata
|
|
160
361
|
*/
|
|
161
362
|
export type BaseMetadata = {
|
|
162
363
|
/** Unique identifier for the unit (replaces tag) */
|
|
163
364
|
name: string;
|
|
164
365
|
};
|
|
165
366
|
/**
|
|
166
|
-
* Metadata type for units with type information.
|
|
367
|
+
* Metadata type for units with explicit type information.
|
|
167
368
|
*
|
|
168
369
|
* For primitive types, `type` is the type name string (e.g., `'number'`).
|
|
169
370
|
* For non-primitive types, `type` IS the actual value:
|
|
@@ -171,15 +372,77 @@ export type BaseMetadata = {
|
|
|
171
372
|
* - Class: the class constructor
|
|
172
373
|
* - Record: the schema object `{ x: 'number', y: 'string' }`
|
|
173
374
|
* - Tuple: the tuple schema array `['number', 'string']`
|
|
375
|
+
*
|
|
376
|
+
* @template T - The `SupportedType` that this metadata describes
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```typescript
|
|
380
|
+
* // Primitive
|
|
381
|
+
* const CelsiusMeta = { name: 'Celsius' as const, type: 'number' as const } satisfies TypedMetadata<number>;
|
|
382
|
+
*
|
|
383
|
+
* // Enum
|
|
384
|
+
* enum Direction { North, South, East, West }
|
|
385
|
+
* const DirectionMeta = { name: 'Direction' as const, type: Direction } satisfies TypedMetadata<typeof Direction>;
|
|
386
|
+
*
|
|
387
|
+
* // Record schema
|
|
388
|
+
* const PointMeta = { name: 'Point' as const, type: { x: 'number', y: 'number' } as const } satisfies TypedMetadata<{ x: number; y: number }>;
|
|
389
|
+
* ```
|
|
390
|
+
*
|
|
391
|
+
* @useWhen You want `WithTypedUnits<M>` to automatically resolve the correct
|
|
392
|
+
* base type from the metadata, avoiding the need to specify it manually.
|
|
393
|
+
*
|
|
394
|
+
* @pitfalls
|
|
395
|
+
* NEVER widen the `type` field to a general `string` or `object` — the type
|
|
396
|
+
* inference chain from `TypedMetadata` → `WithTypedUnits` → `UnitAccessor`
|
|
397
|
+
* depends on the literal or structural type being preserved at compile time.
|
|
398
|
+
*
|
|
399
|
+
* @config
|
|
400
|
+
* Additional fields beyond `name` and `type` (e.g., `symbol`, `description`)
|
|
401
|
+
* are permitted and accessible via the registry accessor.
|
|
402
|
+
*
|
|
403
|
+
* @category Metadata
|
|
404
|
+
* @see WithTypedUnits
|
|
174
405
|
*/
|
|
175
406
|
export type TypedMetadata<T extends SupportedType> = Simplify<{
|
|
176
407
|
name: string;
|
|
177
408
|
type: T extends PrimitiveType ? ToPrimitiveTypeName<T> : T;
|
|
178
409
|
}>;
|
|
179
410
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
411
|
+
* Display and descriptive metadata that can be attached to units in the registry.
|
|
412
|
+
*
|
|
413
|
+
* Supports common properties like abbreviation, format template, description,
|
|
414
|
+
* and symbol, plus an index signature for arbitrary custom fields.
|
|
415
|
+
*
|
|
416
|
+
* @remarks
|
|
417
|
+
* `UnitMetadata` is the internal store type used by `ConverterRegistryImpl`.
|
|
418
|
+
* Access registered metadata via the unit accessor:
|
|
419
|
+
* `registry.Celsius.symbol`, `registry.Celsius.abbreviation`, etc.
|
|
420
|
+
*
|
|
421
|
+
* @example
|
|
422
|
+
* ```typescript
|
|
423
|
+
* const registry = createRegistry()
|
|
424
|
+
* .register({
|
|
425
|
+
* name: 'Celsius' as const,
|
|
426
|
+
* symbol: '°C',
|
|
427
|
+
* abbreviation: '°C',
|
|
428
|
+
* description: 'Degrees Celsius',
|
|
429
|
+
* });
|
|
430
|
+
*
|
|
431
|
+
* registry.Celsius.symbol; // '°C'
|
|
432
|
+
* registry.Celsius.description; // 'Degrees Celsius'
|
|
433
|
+
* ```
|
|
434
|
+
*
|
|
435
|
+
* @config
|
|
436
|
+
* - `symbol` — SI / conventional symbol (e.g., `'°C'`, `'m'`, `'kg'`)
|
|
437
|
+
* - `abbreviation` — short display abbreviation
|
|
438
|
+
* - `format` — optional format template string
|
|
439
|
+
* - `description` — human-readable description
|
|
440
|
+
* - `[key: string]` — any additional custom fields
|
|
441
|
+
*
|
|
442
|
+
* @useWhen You want to attach human-readable labels or display hints to a
|
|
443
|
+
* unit so consumers can render values without hard-coding strings.
|
|
444
|
+
*
|
|
445
|
+
* @category Metadata
|
|
183
446
|
*/
|
|
184
447
|
export interface UnitMetadata {
|
|
185
448
|
/** Short abbreviation for the unit (e.g., "°C", "m", "kg") */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEhF;;;;GAIG;AACH,KAAK,aAAa,GAAG,MAAM,CAAC,OAAO,EAAE,OAAO,KAAK,EAAE,GAAG,CAAC,CAAC;AAExD,eAAO,MAAM,KAAK,EAAE,OAAO,MAAwB,CAAC;AAEpD,eAAO,MAAM,UAAU,EAAE,OAAO,MAA6B,CAAC;AAE9D
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
* Core type branding utilities for unit and format safety
|
|
3
3
|
* @packageDocumentation
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Internal phantom-type brand key used to tag values with unit metadata.
|
|
7
|
+
* Consumers should not use this symbol directly — use `WithUnits<T, M>` instead.
|
|
8
|
+
*
|
|
9
|
+
* @category Branding
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
5
12
|
export const UNITS = Symbol('UNITS');
|
|
13
|
+
/**
|
|
14
|
+
* Reserved symbol for future definition attachment on unit metadata.
|
|
15
|
+
* Not used in the current public API.
|
|
16
|
+
*
|
|
17
|
+
* @category Branding
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
6
20
|
export const DEFINITION = Symbol('DEFINITION');
|
|
7
21
|
//# sourceMappingURL=types.js.map
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,MAAM,CAAC,MAAM,KAAK,GAAkB,MAAM,CAAC,OAAO,CAAC,CAAC;AAEpD,MAAM,CAAC,MAAM,UAAU,GAAkB,MAAM,CAAC,YAAY,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAkB,MAAM,CAAC,OAAO,CAAC,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,UAAU,GAAkB,MAAM,CAAC,YAAY,CAAC,CAAC"}
|
package/dist/utils/graph.d.ts
CHANGED
|
@@ -6,21 +6,83 @@ import type { Converter } from '../converters.js';
|
|
|
6
6
|
/**
|
|
7
7
|
* Find the shortest path between two nodes using BFS.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @
|
|
14
|
-
* @
|
|
9
|
+
* Traverses the unit conversion graph breadth-first to find the minimum-hop
|
|
10
|
+
* path from `from` to `to`. The result is used by `composeConverters` to build
|
|
11
|
+
* a composed converter function for multi-hop conversions.
|
|
12
|
+
*
|
|
13
|
+
* @param from - Starting node (unit name key)
|
|
14
|
+
* @param to - Target node (unit name key)
|
|
15
|
+
* @param adjacencyMap - Graph represented as a nested adjacency list
|
|
16
|
+
* @returns Array of nodes representing the shortest path (including `from` and `to`),
|
|
17
|
+
* or `null` if no path exists
|
|
18
|
+
*
|
|
19
|
+
* @throws {CycleError} If `from === to` (self-conversion cycle)
|
|
20
|
+
* @throws {MaxDepthError} If any discovered path would exceed `MAX_DEPTH` (5) edges
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* The BFS visits each node at most once (`visited` set). Self-loops are detected
|
|
24
|
+
* eagerly (`from === to` check) rather than during traversal.
|
|
25
|
+
*
|
|
26
|
+
* The maximum depth limit (`MAX_DEPTH = 5`) guards against sparse but deeply
|
|
27
|
+
* connected graphs. If you regularly need 5+ hop chains, redesign the graph by
|
|
28
|
+
* registering intermediate direct edges or using `allow()` to pre-compose paths.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* // Internal usage in ConverterRegistryImpl.getConverter():
|
|
33
|
+
* const path = findShortestPath('Celsius', 'Fahrenheit', graph);
|
|
34
|
+
* // path = ['Celsius', 'Kelvin', 'Fahrenheit'] if only C→K and K→F are registered
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @pitfalls
|
|
38
|
+
* NEVER call this function directly from application code — use
|
|
39
|
+
* `registry.convert(value, from).to(to)` or `registry.getConverter(from, to)`,
|
|
40
|
+
* which add BFS caching and handle error propagation correctly.
|
|
41
|
+
*
|
|
42
|
+
* @category Graph
|
|
43
|
+
* @see composeConverters
|
|
15
44
|
*/
|
|
16
45
|
export declare function findShortestPath(from: PropertyKey, to: PropertyKey, adjacencyMap: Map<PropertyKey, Map<PropertyKey, unknown>>): PropertyKey[] | null;
|
|
17
46
|
/**
|
|
18
47
|
* Compose multiple converters along a path into a single converter.
|
|
19
48
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @
|
|
49
|
+
* Given a path `[A, B, C]` and a registry containing `A→B` and `B→C`
|
|
50
|
+
* converters, produces a single `Converter<A, C>` that applies them left-to-right.
|
|
51
|
+
*
|
|
52
|
+
* @param path - Array of unit-name nodes representing the conversion path
|
|
53
|
+
* (at least 2 elements required: `[from, ..., to]`)
|
|
54
|
+
* @param registry - Adjacency map of converters keyed by source unit name
|
|
55
|
+
* @returns Composed converter function that applies each step in sequence
|
|
56
|
+
*
|
|
57
|
+
* @throws {Error} If the path has fewer than 2 nodes
|
|
58
|
+
* @throws {Error} If any converter along the path is missing from the registry
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* The composition is a simple `reduce` — values pass through each converter in
|
|
62
|
+
* order. Each intermediate result is an unbranded `any` at runtime; the type
|
|
63
|
+
* safety is enforced at the call site by the registry's type signature.
|
|
64
|
+
*
|
|
65
|
+
* Floating-point precision accumulates with each hop. For chains where precision
|
|
66
|
+
* matters, prefer registering a direct edge over relying on BFS composition.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* // Internal usage: path found by findShortestPath
|
|
71
|
+
* const composed = composeConverters(['Celsius', 'Kelvin', 'Fahrenheit'], graph);
|
|
72
|
+
* composed(0); // applies C→K then K→F, returning 32
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @pitfalls
|
|
76
|
+
* NEVER call this function with a path containing a unit that has no outgoing
|
|
77
|
+
* edge in the registry — it throws immediately with a non-descriptive `Error`.
|
|
78
|
+
* Always confirm paths with `findShortestPath` first.
|
|
79
|
+
*
|
|
80
|
+
* NEVER rely on composed-converter precision for high-accuracy domains (e.g.,
|
|
81
|
+
* financial or scientific calculations) — each hop introduces floating-point
|
|
82
|
+
* rounding error. Register a direct converter instead.
|
|
83
|
+
*
|
|
84
|
+
* @category Graph
|
|
85
|
+
* @see findShortestPath
|
|
24
86
|
*/
|
|
25
87
|
export declare function composeConverters(path: PropertyKey[], registry: Map<PropertyKey, Map<PropertyKey, Converter<any, any>>>): Converter<any, any>;
|
|
26
88
|
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/utils/graph.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAOlD
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/utils/graph.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAOlD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,WAAW,EACjB,EAAE,EAAE,WAAW,EACf,YAAY,EAAE,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,GACxD,WAAW,EAAE,GAAG,IAAI,CA8CtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,WAAW,EAAE,EACnB,QAAQ,EAAE,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,GAChE,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CA4BrB"}
|
package/dist/utils/graph.js
CHANGED
|
@@ -10,12 +10,41 @@ const MAX_DEPTH = 5;
|
|
|
10
10
|
/**
|
|
11
11
|
* Find the shortest path between two nodes using BFS.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* @
|
|
18
|
-
* @
|
|
13
|
+
* Traverses the unit conversion graph breadth-first to find the minimum-hop
|
|
14
|
+
* path from `from` to `to`. The result is used by `composeConverters` to build
|
|
15
|
+
* a composed converter function for multi-hop conversions.
|
|
16
|
+
*
|
|
17
|
+
* @param from - Starting node (unit name key)
|
|
18
|
+
* @param to - Target node (unit name key)
|
|
19
|
+
* @param adjacencyMap - Graph represented as a nested adjacency list
|
|
20
|
+
* @returns Array of nodes representing the shortest path (including `from` and `to`),
|
|
21
|
+
* or `null` if no path exists
|
|
22
|
+
*
|
|
23
|
+
* @throws {CycleError} If `from === to` (self-conversion cycle)
|
|
24
|
+
* @throws {MaxDepthError} If any discovered path would exceed `MAX_DEPTH` (5) edges
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* The BFS visits each node at most once (`visited` set). Self-loops are detected
|
|
28
|
+
* eagerly (`from === to` check) rather than during traversal.
|
|
29
|
+
*
|
|
30
|
+
* The maximum depth limit (`MAX_DEPTH = 5`) guards against sparse but deeply
|
|
31
|
+
* connected graphs. If you regularly need 5+ hop chains, redesign the graph by
|
|
32
|
+
* registering intermediate direct edges or using `allow()` to pre-compose paths.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // Internal usage in ConverterRegistryImpl.getConverter():
|
|
37
|
+
* const path = findShortestPath('Celsius', 'Fahrenheit', graph);
|
|
38
|
+
* // path = ['Celsius', 'Kelvin', 'Fahrenheit'] if only C→K and K→F are registered
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @pitfalls
|
|
42
|
+
* NEVER call this function directly from application code — use
|
|
43
|
+
* `registry.convert(value, from).to(to)` or `registry.getConverter(from, to)`,
|
|
44
|
+
* which add BFS caching and handle error propagation correctly.
|
|
45
|
+
*
|
|
46
|
+
* @category Graph
|
|
47
|
+
* @see composeConverters
|
|
19
48
|
*/
|
|
20
49
|
export function findShortestPath(from, to, adjacencyMap) {
|
|
21
50
|
// Handle self-conversion (cycle detection)
|
|
@@ -59,10 +88,43 @@ export function findShortestPath(from, to, adjacencyMap) {
|
|
|
59
88
|
/**
|
|
60
89
|
* Compose multiple converters along a path into a single converter.
|
|
61
90
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* @
|
|
91
|
+
* Given a path `[A, B, C]` and a registry containing `A→B` and `B→C`
|
|
92
|
+
* converters, produces a single `Converter<A, C>` that applies them left-to-right.
|
|
93
|
+
*
|
|
94
|
+
* @param path - Array of unit-name nodes representing the conversion path
|
|
95
|
+
* (at least 2 elements required: `[from, ..., to]`)
|
|
96
|
+
* @param registry - Adjacency map of converters keyed by source unit name
|
|
97
|
+
* @returns Composed converter function that applies each step in sequence
|
|
98
|
+
*
|
|
99
|
+
* @throws {Error} If the path has fewer than 2 nodes
|
|
100
|
+
* @throws {Error} If any converter along the path is missing from the registry
|
|
101
|
+
*
|
|
102
|
+
* @remarks
|
|
103
|
+
* The composition is a simple `reduce` — values pass through each converter in
|
|
104
|
+
* order. Each intermediate result is an unbranded `any` at runtime; the type
|
|
105
|
+
* safety is enforced at the call site by the registry's type signature.
|
|
106
|
+
*
|
|
107
|
+
* Floating-point precision accumulates with each hop. For chains where precision
|
|
108
|
+
* matters, prefer registering a direct edge over relying on BFS composition.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* // Internal usage: path found by findShortestPath
|
|
113
|
+
* const composed = composeConverters(['Celsius', 'Kelvin', 'Fahrenheit'], graph);
|
|
114
|
+
* composed(0); // applies C→K then K→F, returning 32
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @pitfalls
|
|
118
|
+
* NEVER call this function with a path containing a unit that has no outgoing
|
|
119
|
+
* edge in the registry — it throws immediately with a non-descriptive `Error`.
|
|
120
|
+
* Always confirm paths with `findShortestPath` first.
|
|
121
|
+
*
|
|
122
|
+
* NEVER rely on composed-converter precision for high-accuracy domains (e.g.,
|
|
123
|
+
* financial or scientific calculations) — each hop introduces floating-point
|
|
124
|
+
* rounding error. Register a direct converter instead.
|
|
125
|
+
*
|
|
126
|
+
* @category Graph
|
|
127
|
+
* @see findShortestPath
|
|
66
128
|
*/
|
|
67
129
|
export function composeConverters(path, registry) {
|
|
68
130
|
if (path.length < 2) {
|