@vicin/phantom 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +869 -0
  3. package/dist/assertors/assertors.d.ts +86 -0
  4. package/dist/assertors/assertors.d.ts.map +1 -0
  5. package/dist/assertors/assertors.js +91 -0
  6. package/dist/assertors/assertors.js.map +1 -0
  7. package/dist/assertors/brand.d.ts +13 -0
  8. package/dist/assertors/brand.d.ts.map +1 -0
  9. package/dist/assertors/brand.js +12 -0
  10. package/dist/assertors/brand.js.map +1 -0
  11. package/dist/assertors/identity.d.ts +13 -0
  12. package/dist/assertors/identity.d.ts.map +1 -0
  13. package/dist/assertors/identity.js +12 -0
  14. package/dist/assertors/identity.js.map +1 -0
  15. package/dist/assertors/index.d.ts +2 -0
  16. package/dist/assertors/index.d.ts.map +1 -0
  17. package/dist/assertors/index.js +2 -0
  18. package/dist/assertors/index.js.map +1 -0
  19. package/dist/assertors/trait.d.ts +38 -0
  20. package/dist/assertors/trait.d.ts.map +1 -0
  21. package/dist/assertors/trait.js +37 -0
  22. package/dist/assertors/trait.js.map +1 -0
  23. package/dist/assertors/transformation.d.ts +27 -0
  24. package/dist/assertors/transformation.d.ts.map +1 -0
  25. package/dist/assertors/transformation.js +26 -0
  26. package/dist/assertors/transformation.js.map +1 -0
  27. package/dist/chain/chain.d.ts +42 -0
  28. package/dist/chain/chain.d.ts.map +1 -0
  29. package/dist/chain/chain.js +48 -0
  30. package/dist/chain/chain.js.map +1 -0
  31. package/dist/chain/index.d.ts +2 -0
  32. package/dist/chain/index.d.ts.map +1 -0
  33. package/dist/chain/index.js +2 -0
  34. package/dist/chain/index.js.map +1 -0
  35. package/dist/index.d.ts +7 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +6 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/phantom.d.ts +352 -0
  40. package/dist/phantom.d.ts.map +1 -0
  41. package/dist/phantom.js +139 -0
  42. package/dist/phantom.js.map +1 -0
  43. package/dist/types/composed/brand.d.ts +32 -0
  44. package/dist/types/composed/brand.d.ts.map +1 -0
  45. package/dist/types/composed/brand.js +2 -0
  46. package/dist/types/composed/brand.js.map +1 -0
  47. package/dist/types/composed/helpers.d.ts +75 -0
  48. package/dist/types/composed/helpers.d.ts.map +1 -0
  49. package/dist/types/composed/helpers.js +2 -0
  50. package/dist/types/composed/helpers.js.map +1 -0
  51. package/dist/types/composed/identity.d.ts +37 -0
  52. package/dist/types/composed/identity.d.ts.map +1 -0
  53. package/dist/types/composed/identity.js +2 -0
  54. package/dist/types/composed/identity.js.map +1 -0
  55. package/dist/types/composed/index.d.ts +6 -0
  56. package/dist/types/composed/index.d.ts.map +1 -0
  57. package/dist/types/composed/index.js +2 -0
  58. package/dist/types/composed/index.js.map +1 -0
  59. package/dist/types/composed/trait.d.ts +35 -0
  60. package/dist/types/composed/trait.d.ts.map +1 -0
  61. package/dist/types/composed/trait.js +2 -0
  62. package/dist/types/composed/trait.js.map +1 -0
  63. package/dist/types/composed/transformation.d.ts +34 -0
  64. package/dist/types/composed/transformation.d.ts.map +1 -0
  65. package/dist/types/composed/transformation.js +2 -0
  66. package/dist/types/composed/transformation.js.map +1 -0
  67. package/dist/types/core/base.d.ts +35 -0
  68. package/dist/types/core/base.d.ts.map +1 -0
  69. package/dist/types/core/base.js +2 -0
  70. package/dist/types/core/base.js.map +1 -0
  71. package/dist/types/core/index.d.ts +8 -0
  72. package/dist/types/core/index.d.ts.map +1 -0
  73. package/dist/types/core/index.js +2 -0
  74. package/dist/types/core/index.js.map +1 -0
  75. package/dist/types/core/input.d.ts +38 -0
  76. package/dist/types/core/input.d.ts.map +1 -0
  77. package/dist/types/core/input.js +2 -0
  78. package/dist/types/core/input.js.map +1 -0
  79. package/dist/types/core/label.d.ts +34 -0
  80. package/dist/types/core/label.d.ts.map +1 -0
  81. package/dist/types/core/label.js +2 -0
  82. package/dist/types/core/label.js.map +1 -0
  83. package/dist/types/core/phantom.d.ts +20 -0
  84. package/dist/types/core/phantom.d.ts.map +1 -0
  85. package/dist/types/core/phantom.js +11 -0
  86. package/dist/types/core/phantom.js.map +1 -0
  87. package/dist/types/core/tag.d.ts +35 -0
  88. package/dist/types/core/tag.d.ts.map +1 -0
  89. package/dist/types/core/tag.js +2 -0
  90. package/dist/types/core/tag.js.map +1 -0
  91. package/dist/types/core/traits.d.ts +51 -0
  92. package/dist/types/core/traits.d.ts.map +1 -0
  93. package/dist/types/core/traits.js +2 -0
  94. package/dist/types/core/traits.js.map +1 -0
  95. package/dist/types/core/variants.d.ts +34 -0
  96. package/dist/types/core/variants.d.ts.map +1 -0
  97. package/dist/types/core/variants.js +2 -0
  98. package/dist/types/core/variants.js.map +1 -0
  99. package/dist/types/errors.d.ts +40 -0
  100. package/dist/types/errors.d.ts.map +1 -0
  101. package/dist/types/errors.js +2 -0
  102. package/dist/types/errors.js.map +1 -0
  103. package/dist/types/index.d.ts +4 -0
  104. package/dist/types/index.d.ts.map +1 -0
  105. package/dist/types/index.js +2 -0
  106. package/dist/types/index.js.map +1 -0
  107. package/dist/types/public.d.ts +224 -0
  108. package/dist/types/public.d.ts.map +1 -0
  109. package/dist/types/public.js +12 -0
  110. package/dist/types/public.js.map +1 -0
  111. package/package.json +57 -0
package/README.md ADDED
@@ -0,0 +1,869 @@
1
+ # Phantom
2
+
3
+ `Phantom` is a powerful, lightweight TypeScript library for nominal typing. it uses **type-only** metadata object that can be attached to any type with clean IDE. It enables compile-time distinctions between structurally identical types (e.g., `Email` vs. `Username` as branded strings) while supporting advanced features like **constrained identities**,**state variants**, **additive traits**, and **reversible transformations**. making it ideal for domain-driven design (DDD), APIs, and type-safe primitives with minimal performance overhead.
4
+
5
+ ---
6
+
7
+ ## Table of contents
8
+
9
+ - [Features](#features)
10
+ - [Install](#install)
11
+ - [Core concepts](#core-concepts)
12
+ - [Phantom](#phantom-1)
13
+ - [Chaining](#chaining)
14
+ - [Hot-path code ( truly type-only pattern )](#hot-path-code--truly-type-only-pattern-)
15
+ - [Debugging](#debugging)
16
+ - [API reference](#api-reference)
17
+ - [Full examples](#full-examples)
18
+ - [Sigil](#sigil)
19
+ - [Contributing](#contributing)
20
+ - [License](#license)
21
+ - [Author](#author)
22
+
23
+ ## Features
24
+
25
+ - **Nominal branding for primitives and objects to prevent accidental misuse (e.g., UserId ≠ PostId).**
26
+
27
+ - **Constrained identities with base types and variants for enforced type safety.**
28
+
29
+ - **Traits as independent, additive metadata sets for capabilities or flags.**
30
+
31
+ - **Transformations for reversible type changes (e.g., encrypt/decrypt) while tracking origins.**
32
+
33
+ - **assertors as zero-cost runtime helpers for applying metadata via type casts.**
34
+
35
+ - **Modular namespaces: So all devs are happy.**
36
+
37
+ ---
38
+
39
+ ## Install
40
+
41
+ ```Bash
42
+ npm install @vicin/phantom
43
+ # or
44
+ yarn add @vicin/phantom
45
+ # or
46
+ pnpm add @vicin/phantom
47
+ ```
48
+
49
+ **Requirements:** TypeScript 5.0+ recommended.
50
+
51
+ ---
52
+
53
+ ## Core concepts
54
+
55
+ ### Terminology
56
+
57
+ - **Label:** Optional human-readable description (e.g., "User ID")—does not affect typing.
58
+
59
+ - **Tag:** Unique nominal identifier (string/symbol) for brands/identities/transformations.
60
+
61
+ - **Variants:** Mutually exclusive states (e.g., "Verified" | "Unverified") for identities/transformations.
62
+
63
+ - **Base:** Runtime type constraint (e.g., string) enforced on assignment.
64
+
65
+ - **Input:** Original type preserved in transformations for reversion.
66
+
67
+ - **Traits:** Key-value map of capabilities (e.g., { PII: true }) that can be added/removed independently.
68
+
69
+ - **Brand:** Basic nominal tag with optional label.
70
+
71
+ - **Identity:** Brand + base/variants for constrained nominals.
72
+
73
+ - **Trait:** Additive metadata flag.
74
+
75
+ - **Transformation:** Reversible change with input tracking.
76
+
77
+ ---
78
+
79
+ ### \_\_Phantom object
80
+
81
+ Under the hood `Phantom` is just **type-only** metadata object appended to your types and gets updated every time one of phantom types is used. This allows mimicking nominal typing inside TypeScript's structural typing.
82
+
83
+ When `Phantom` is used with it's full potential the IDE will show you something like this:
84
+
85
+ ```ts
86
+ type X = string & {
87
+ __Phantom: {
88
+ __Tag: 'X';
89
+ __Label?: 'Label of X';
90
+ __Variants: 'Variant1' | 'Variant2';
91
+ __Traits: {
92
+ trait1: true;
93
+ trait2: true;
94
+ };
95
+ __Input: SomeInput;
96
+ __OriginalType?: string;
97
+ };
98
+ };
99
+ ```
100
+
101
+ From the first glance you can spot that`Phantom` is designed to separate original type `string` from `__Phantom` metadata object so your IDE can stay clean, each Phantom object will have these fields:
102
+
103
+ - **\_\_Tag:** Nominal tag (**identity**) of the type which tells who this value is. can be `string` | `symbol`.
104
+
105
+ - **\_\_Label?:** Optional label describing this nominal identity.
106
+
107
+ - **\_\_Variants:** Optional states of the nominal identity.
108
+
109
+ - **\_\_Traits:** Additional properties the type holds, unlike `__Tag` that tells who `__Traits` tell what this type hold or can do (e.g. `Validated`, `PII` etc...).
110
+
111
+ - **\_\_Input:** Input type in transformations (e.g. Transformation `Encrypted` can have `__Input: string` which means that encrypted value is `string`).
112
+
113
+ - **\_\_OriginalType?:** Type without `__Phantom` metadata object. related to internal implementation only and is crucial to keep clean IDE messages.
114
+
115
+ More details of these properties will be discussed later.
116
+
117
+ ---
118
+
119
+ ## Phantom
120
+
121
+ ### Branding
122
+
123
+ Use brands for simple nominal distinctions on primitives.
124
+
125
+ #### Structure
126
+
127
+ **`Brand.Declare<Tag, Label?>`**
128
+
129
+ - **Tag:** Unique identifier which can string or symbol.
130
+ - **Label?:** Optional label description. can be set to `never`.
131
+
132
+ #### Basic example
133
+
134
+ ```ts
135
+ import { Phantom } from '@vicin/phantom';
136
+
137
+ // Declare a brand
138
+ type UserId = Phantom.Brand.Declare<'UserId', 'Unique user identifier'>;
139
+
140
+ // Assertor for assigning the brand
141
+ const asUserId = Phantom.assertors.asBrand<UserId>();
142
+
143
+ // Usage
144
+ const id: string = '123';
145
+ const userId = asUserId(id); // type: string & { __Phantom: { __Tag: "UserId"; __Label?: "Unique user identifier"; __OriginalType: string; } }
146
+
147
+ // Type safety: prevents assignment
148
+ let postId: string = 'abc';
149
+ postId = userId; // Type error: 'UserId' is not assignable to unbranded string
150
+ ```
151
+
152
+ #### Re-brand
153
+
154
+ Branding already branded value is prohibited and results in Error type:
155
+
156
+ ```ts
157
+ const value = '123';
158
+
159
+ // Branding string as UserId
160
+ const userId = asUserId(value);
161
+
162
+ // Trying to brand userId as PostId returns type error
163
+ const postId = asPostId(userId); // type: Flow.TypeError<{ code: "ALREADY_BRANDED"; message: "Type already branded"; context: { type: <userId type>; }; }>
164
+ ```
165
+
166
+ ---
167
+
168
+ ### Identities with constraints
169
+
170
+ `Identity` is just `Brand` with extra capabilities as `Base` and `Variants` for more complex use cases.
171
+
172
+ #### Structure
173
+
174
+ **`Identity.Declare<Tag, Label?, Base?, Variants?>`**
175
+
176
+ - **Tag:** Unique identifier which can string or symbol.
177
+ - **Label?:** Optional label description. can be set to `never`.
178
+ - **Base?:** Optional base that constrains identity so only `string` for example can be casted as `Email`. can be set to `never`.
179
+ - **Variants?:** Optional variants (states) of the identity. can be set to `never`.
180
+
181
+ #### Basic example
182
+
183
+ ```ts
184
+ import { Phantom } from '@vicin/phantom';
185
+
186
+ // Declare an identity with base (string) and variants
187
+ type Email = Phantom.Identity.Declare<
188
+ 'Email',
189
+ 'User email address',
190
+ string,
191
+ 'Verified' | 'Unverified'
192
+ >;
193
+
194
+ // Assertor
195
+ const asEmail = Phantom.assertors.asIdentity<Email>();
196
+
197
+ // Usage
198
+ const email = asEmail('user@example.com'); // type: string & { __Phantom: { __Tag: "Email"; __Label?: "User email address"; __Variants: "Verified" | "Unverified"; __OriginalType: string; } }
199
+ ```
200
+
201
+ #### Re-brand
202
+
203
+ Branding already branded value is prohibited and results in Error type:
204
+
205
+ ```ts
206
+ const value = '123';
207
+
208
+ // Branding string as UserId
209
+ const userId = asUserId(value);
210
+
211
+ // Trying to brand userId as PostId returns type error
212
+ const postId = asPostId(userId); // type: Flow.TypeError<{ code: "ALREADY_BRANDED"; message: "Type already branded"; context: { type: <userId type>; }; }>
213
+ ```
214
+
215
+ #### Base
216
+
217
+ ```ts
218
+ const validBase = asEmail('user@example.com');
219
+
220
+ const inValidBase = asEmail(123); // type: Flow.TypeError<{ code: "TYPE_NOT_EXTEND_BASE"; message: "Type not extend base"; context: { type: 123; base: string; }; }>
221
+ ```
222
+
223
+ #### Variants
224
+
225
+ ```ts
226
+ // With variant
227
+ type VerifiedEmail = Phantom.Identity.WithVariant<Email, 'Verified'>;
228
+ const asVerifiedEmail = Phantom.assertors.asIdentity<VerifiedEmail>();
229
+
230
+ // Usage
231
+ const verifiedEmail = asVerifiedEmail('user@verified.com'); // type: string & { __Phantom: { __Tag: "Email"; __Label?: "User email address"; __Variants: "Verified"; __OriginalType: string; } }
232
+
233
+ // Behavior
234
+ type test1 = VerifiedEmail extends Email ? true : false; // true as all VerifiedEmails are Emails
235
+ type test2 = Email extends VerifiedEmail ? true : false; // false as not all Emails are VerifiedEmails
236
+ ```
237
+
238
+ ---
239
+
240
+ ### Traits (additive capabilities)
241
+
242
+ Traits allow independent metadata addition/removal.
243
+
244
+ #### Structure
245
+
246
+ **`Trait.Declare<Trait>`**
247
+
248
+ - **Trait:** Unique identifier which can string or symbol.
249
+
250
+ #### Basic example
251
+
252
+ ```ts
253
+ import { Phantom } from '@vicin/phantom';
254
+
255
+ // types
256
+ type PII = Phantom.Trait.Declare<'PII'>;
257
+
258
+ // assertors
259
+ const addPII = Phantom.assertors.addTrait<PII>();
260
+ const dropPII = Phantom.assertors.dropTrait<PII>();
261
+
262
+ // add trait
263
+ let location = 'location';
264
+ location = addPII(location); // type: string & { __Phantom: { __Traits: { PII: true; }; __OriginalType: string; } }
265
+
266
+ // drop trait
267
+ location = dropPII(location); // type: string
268
+ ```
269
+
270
+ #### Multi add and drop
271
+
272
+ ```ts
273
+ // types
274
+ type PII = Phantom.Trait.Declare<'PII'>;
275
+ type Validated = Phantom.Trait.Declare<'Validated'>;
276
+
277
+ // assertors
278
+ const addValidatedPII = Phantom.assertors.addTraits<[PII, Validated]>();
279
+ const dropValidatedPII = Phantom.assertors.dropTraits<[PII, Validated]>();
280
+
281
+ // add traits
282
+ let location = 'location';
283
+ location = addValidatedPII(location); // type: string & { __Phantom: { __Traits: { PII: true; Validated: true; }; __OriginalType: string; } }
284
+
285
+ // drop traits
286
+ location = dropValidatedPII(location); // type: string
287
+ ```
288
+
289
+ ---
290
+
291
+ ### Transformations (Type pipe-like behavior)
292
+
293
+ Traits allow complex type transformations that remember original value, it defines new identity of the value (e.g. `Encoded`) while storing original input type (e.g. `string`).
294
+
295
+ So basically you have:
296
+
297
+ - **Transformed:** New type after transformation (e.g. `Uint8Array` after `Encoding`).
298
+ - **Input:** Original type of value (e.g. `string`, `number` or whatever value that is `Encoded`).
299
+
300
+ Type identity is `Transformed` and stores `Input` under special field inside `__Phantom` which is `__Input`.
301
+
302
+ #### Structure
303
+
304
+ **`Transformation.Declare<Input, Tag, Label?, Base?, Variants?>`**
305
+
306
+ - **Input:** Type input to the transformation.
307
+ - **Tag:** Unique identifier of transformed which can string or symbol.
308
+ - **Label?:** Optional label description of transformed. can be set to `never`.
309
+ - **Base?:** Optional base that constrains identity of transformed so only `Uint8Array` for example can be casted as `Encoded`. can be set to `never`.
310
+ - **Variants?:** Optional variants (states) of the identity. can be set to `never`.
311
+
312
+ #### Basic example
313
+
314
+ ```ts
315
+ import { Phantom } from '@vicin/phantom';
316
+
317
+ // type
318
+ type Encoded<I = unknown> = Phantom.Transformation.Declare<
319
+ I,
320
+ 'Encoded',
321
+ 'Encoded value',
322
+ Uint8Array // Encoded value should be 'Uint8Array'
323
+ >;
324
+
325
+ // assertors
326
+ const applyEncode = Phantom.assertors.applyTransformation<Encoded>();
327
+ const revertEncode = Phantom.assertors.revertTransformation<Encoded>();
328
+ ```
329
+
330
+ As you have seen the type `Encoded` is generic, so we can pass `Input` to it and represent any type we want to mark as being `Encoded`.
331
+
332
+ #### Apply / Revert pattern
333
+
334
+ Now we define our `Encode` function to apply and revert transformation:
335
+
336
+ ```ts
337
+ function encode<V>(value: V) {
338
+ const encoded = value as Uint8Array; // replace with actual encoding
339
+ return applyEncode(value, encoded);
340
+ }
341
+
342
+ function decode<E extends Encoded>(encoded: E) {
343
+ const decoded = encoded; // replace with actual decoding
344
+ return revertEncode(encoded, decoded);
345
+ }
346
+
347
+ const value = 'some value';
348
+ const encoded = encode(value); // type: Uint8Array<ArrayBufferLike> & { __Phantom: { __Input: string; __Tag: "Encoded"; __Label?: "Encoded value"; __OriginalType?: Uint8Array<ArrayBufferLike>; }; }
349
+ const decoded = decode(encoded); // type: string
350
+ ```
351
+
352
+ #### Repeated Apply / Revert
353
+
354
+ You can apply single transformation multiple times on a value and `Phantom` will store value from each step as an `Input` to the next step, so if you encoded single value 2 times you will need to decode it 2 times as well to get the original type.
355
+
356
+ ```ts
357
+ const value = 'some value';
358
+ const encoded1 = encode(value); // '__Input' here is 'string'
359
+ const encoded2 = encode(encoded1); // '__Input' here is 'typeof encoded1'
360
+ const decoded1 = decode(encoded2); // type here is 'typeof encoded2'
361
+ const original = decode(decoded1); // type here is 'string'
362
+ ```
363
+
364
+ Same applies to different transformations, so if you `Encoded` then `Encrypted` a value, you will need to `Decrypt` first then `Decode`.
365
+
366
+ #### Base
367
+
368
+ Same behavior as [`Identity`](#identities-with-constraints)
369
+
370
+ #### Variants
371
+
372
+ Same behavior as [`Identity`](#identities-with-constraints)
373
+
374
+ ---
375
+
376
+ ### Errors
377
+
378
+ This library return `ErrorType` instead of using type constraints or returning `never` which makes debugging for errors much easier.
379
+ When passed value violate specific rule, descripted `ErrorType` is returned.
380
+
381
+ **ErrorTypes:**
382
+
383
+ - **ALREADY_BRANDED:** Error returned if already branded value (with either `Brand` or `Identity`) is passed to `.Assign<>` again.
384
+
385
+ - **TYPE_NOT_EXTEND_BASE:** Error returned if type not extend `Base` constraint of `Identity`.
386
+
387
+ - **TRANSFORMATION_MISMATCH:** Error returned if type passed to `Transformation.Revert<>` is different from `Transformation` intended.
388
+
389
+ - **NOT_TRANSFORMED:** Error returned if you tried to revert a non-transformed type.
390
+
391
+ All the types inside `Phantom` check for `ErrorType` first before applying any change, so if you tried to pass `ErrorType` to `Brand.Assign<>` or `asBrand<B>()` for example the error will return unchanged.
392
+
393
+ ---
394
+
395
+ ### Symbols
396
+
397
+ In earlier examples we used strings as our tags just for simplicity, this is acceptable but in real-life projects it's better to use `unique symbol` as a tag so your types are truly unique.
398
+
399
+ ```ts
400
+ import { Phantom } from '@vicin/phantom';
401
+
402
+ // type
403
+ declare const __UserId: unique symbol;
404
+ type UserId = Phantom.Brand.Declare<typeof __UserId, 'User id'>;
405
+
406
+ // assertor
407
+ export const asUserId = Phantom.assertors.asBrand<UserId>();
408
+ ```
409
+
410
+ Now `UserId` can't be re-defined anywhere else in the codebase.
411
+
412
+ ---
413
+
414
+ ### Imports
415
+
416
+ Some devs (including me) may find `Phantom` namespace everywhere repetitive and prefer using `Brand` or `assertors` directly, every namespace under `Phantom` can be imported alone:
417
+
418
+ ```ts
419
+ import { Brand, assertors } from '@vicin/phantom';
420
+
421
+ // type
422
+ declare const __UserId: unique symbol; // declare type only unique symbol to be our tag
423
+ type UserId = Brand.Declare<typeof __UserId, 'User id'>;
424
+
425
+ // assertor
426
+ export const asUserId = assertors.asBrand<UserId>();
427
+ ```
428
+
429
+ Also if you prefer default imports you can just:
430
+
431
+ ```ts
432
+ import P from '@vicin/phantom';
433
+
434
+ // type
435
+ declare const __UserId: unique symbol; // declare type only unique symbol to be our tag
436
+ type UserId = P.Brand.Declare<typeof __UserId, 'User id'>;
437
+
438
+ // assertor
439
+ export const asUserId = P.assertors.asBrand<UserId>();
440
+ ```
441
+
442
+ You are free to pick whatever pattern you are comfortable with.
443
+
444
+ ---
445
+
446
+ ## Chaining
447
+
448
+ If you need to call multiple assertors on a single value (e.g. mark brand and attach two traits) the code can get messy:
449
+
450
+ ```ts
451
+ import { Brand, Trait, assertors } from '@vicin/phantom';
452
+
453
+ type UserId = Brand.Declare<'UserId'>;
454
+ const asUserId = assertors.asBrand<UserId>();
455
+
456
+ type PII = Trait.Declare<'PII'>;
457
+ const addPII = assertors.addTrait<PII>();
458
+
459
+ type Validated = Trait.Declare<'Validated'>;
460
+ const addValidated = assertors.addTrait<Validated>();
461
+
462
+ const userId = addValidated(addPII(asUserId('123'))); // <-- ugly nesting
463
+ ```
464
+
465
+ Instead you can use `PhantomChain` class:
466
+
467
+ ```ts
468
+ import { PhantomChain } from '@vicin/phantom';
469
+
470
+ const userId = new PhantomChain('123')
471
+ .with(asUserId)
472
+ .with(addPII)
473
+ .with(addValidated)
474
+ .end();
475
+ ```
476
+
477
+ The `.with()` is order sensitive, so previous example is equivalent to `addValidated(addPII(asUserId("123")))`. also if any `ErrorType` is retuned at any stage of the chain, the chain will break and error will propagate unchanged making debugging much easier.
478
+
479
+ **Note:**
480
+
481
+ - With every step new `PhantomChain` instance is created, in most apps this is negligible as `PhantomChain` is just simple object with two methods but avoid using it in hot paths.
482
+
483
+ ---
484
+
485
+ ## Hot-path code ( truly type-only pattern )
486
+
487
+ In hot-path code every function call matters, and although assertor functions makes code much cleaner but you may prefer to use `as` in performance critical situations so `Phantom` lives in type system entirely and have zero run-time trace, these examples defines best practices to do so:
488
+
489
+ **Brand**:
490
+
491
+ ```ts
492
+ import { Brand } from '@vicin/phantom';
493
+
494
+ type UserId = Brand.Declare<'UserId'>;
495
+ type AsUserId<T> = Brand.Assign<UserId, T>;
496
+
497
+ const userId = '123' as AsUserId<string>;
498
+ ```
499
+
500
+ **Identity**:
501
+
502
+ ```ts
503
+ import { Identity } from '@vicin/phantom';
504
+
505
+ type PostId = Identity.Declare<'PostId'>;
506
+ type AsPostId<T> = Identity.Assign<UserId, T>;
507
+
508
+ const postId = '123' as AsPostId<string>;
509
+ ```
510
+
511
+ **Trait**:
512
+
513
+ ```ts
514
+ import { Trait } from '@vicin/phantom';
515
+
516
+ type PII = Trait.Declare<'PII'>;
517
+ type AddPII<T> = Trait.Add<PII, T>;
518
+ type DropPII<T> = Trait.Drop<PII, T>;
519
+
520
+ let location1 = 'location' as AddPII<string>;
521
+ let location2 = location1 as DropPII<typeof location1>;
522
+ ```
523
+
524
+ **Transformation**:
525
+
526
+ ```ts
527
+ import { Transformation } from '@vicin/phantom';
528
+
529
+ type Encoded<T> = Transformation.Declare<T, 'Encoded'>;
530
+ type ApplyEncoded<I, T> = Transformation.Apply<Encoded<any>, I, T>;
531
+ type RevertEncoded<T, I> = Transformation.Revert<Encoded<any>, T, I>;
532
+
533
+ function encode<V>(value: V) {
534
+ const encoded = value as Uint8Array; // replace with actual encoding
535
+ return encoded as ApplyEncoded<V, typeof encoded>;
536
+ }
537
+
538
+ function decode<E extends Encoded>(encoded: E) {
539
+ const decoded = encoded as string; // replace with actual decoding
540
+ return decoded as RevertEncoded<E, typeof decoded>;
541
+ }
542
+ ```
543
+
544
+ **Chaining**:
545
+
546
+ Chaining is not supported in **type-only pattern** and nesting is the only way to reliably calculate types:
547
+
548
+ ```ts
549
+ import { Brand, Trait, assertors } from '@vicin/phantom';
550
+
551
+ type UserId = Brand.Declare<'UserId'>;
552
+ type AsUserId<T> = Brand.Assign<UserId, T>;
553
+
554
+ type PII = Trait.Declare<'PII'>;
555
+ type AddPII<T> = Trait.Add<PII, T>;
556
+
557
+ type Validated = Trait.Declare<'Validated'>;
558
+ type AddValidated<T> = Trait.Add<Validated, T>;
559
+
560
+ const userId = '123' as AddValidated<AddPII<AsUserId<string>>>;
561
+ ```
562
+
563
+ **Note:**
564
+
565
+ - Maintainability of code is crucial, Use **type-only pattern** only when you have to, but in most cases it's adviced to use assertor functions.
566
+
567
+ ---
568
+
569
+ ## Debugging
570
+
571
+ When debugging the `__Phantom` object can complicate IDE messages, you can temporarly add `StripPhantom` type or `stripPhantom` function.
572
+
573
+ ---
574
+
575
+ ## API reference
576
+
577
+ ### Core Metadata Namespaces
578
+
579
+ These handle individual metadata fields in the `__Phantom` object.
580
+
581
+ - **Label:** Descriptive strings (optional).
582
+ - `Any`: Marker for labeled types.
583
+ - `LabelOf<T>`: Extract label from `T`.
584
+ - `HasLabel<T, L>`: Check if `T` has label `L`.
585
+
586
+ - **Tag:** Unique nominal identifiers (string/symbol).
587
+ - `Any`: Marker for tagged types.
588
+ - `TagOf<T>`: Extract tag from `T`.
589
+ - `HasTag<T, Ta>`: Check if `T` has tag `Ta`.
590
+
591
+ - **Variants:** Mutually exclusive states (string unions).
592
+ - `Any`: Marker for variant-bearing types.
593
+ - `VariantsOf<T>`: Extract variant union from `T`.
594
+ - `HasVariants<T>`: Check if `T` has variants.
595
+
596
+ - **Base:** Runtime type constraints.
597
+ - `Any`: Marker for base-constrained types.
598
+ - `BaseOf<T>`: Extract base from `T`.
599
+ - `HasBase<T, B>`: Check if `T` has base `B`.
600
+
601
+ - **Input:** Original types in transformations.
602
+ - `Any`: Marker for input-bearing types.
603
+ - `InputOf<T>`: Extract input from `T`.
604
+ - `HasInput<T, I>`: Check if `T` has input `I`.
605
+
606
+ - **Traits:** Additive capability maps (e.g., `{ TraitKey: true }`).
607
+ - `Any`: Marker for trait-bearing types.
608
+ - `TraitsOf<T>`: Extract trait map from `T`.
609
+ - `TraitKeysOf<T>`: Extract trait keys from `T`.
610
+ - `HasTraits<T, Tr>`: Check if `T` has trait `Tr`.
611
+
612
+ ### Feature Namespaces
613
+
614
+ These provide nominal typing and metadata operations.
615
+
616
+ - **Brand:** Simple nominal branding.
617
+ - `Any`: Marker for branded types.
618
+ - `Declare<T, L>`: Declare a brand with tag `T` and optional label `L`.
619
+ - `Assign<B, T>`: Assign brand `B` to `T` (fails if already branded).
620
+ - `AssignSafe<B, T>`: Assign if possible, else return `T`.
621
+ - `isBrand<T, B>`: Check if `T` is branded with `B`.
622
+
623
+ - **Identity**: Brands with bases and variants.
624
+ - `Any`: Marker for identity types.
625
+ - `Declare<T, L, B, V>`: Declare identity with tag `T`, label `L`, base `B`, variants `V`.
626
+ - `Assign<I, T>`: Assign identity `I` to `T` (fails if already branded).
627
+ - `AssignSafe<I, T>`: Safe assignment, return `T` is already has identity.
628
+ - `WithVariant<I, V>`: Set variant `V` on identity `I`.
629
+ - `WithTypeVariant<T, V>`: Set variant `V` on type `T`.
630
+ - `isIdentity<T, I>`: Check if `T` matches identity `I`.
631
+
632
+ - **Trait:** Additive/removable capabilities.
633
+ - `Any`: Marker for trait types.
634
+ - `Declare<Tr>`: Declare trait with key `Tr`.
635
+ - `Add<Tr, T>`: Add trait `Tr` to `T`.
636
+ - `AddMulti<Tr[], T>`: Add multiple traits to `T`.
637
+ - `Drop<Tr, T>`: Remove trait `Tr` from `T`.
638
+ - `DropMulti<Tr[], T>`: Remove multiple traits from `T`.
639
+ - `HasTrait<T, Tr>`: Check if `T` has trait `Tr`.
640
+
641
+ - **Transformation:** Reversible type changes with input tracking.
642
+ - `Any`: Marker for transformation types.
643
+ - `Declare<I, T, L, B, V>`: Declare transformation with input `I`, tag `T`, label `L`, base `B`, variants `V`.
644
+ - `Apply<Tr, I, T>`: Apply transformation `Tr` from input `I` to `T`.
645
+ - `Revert<Tr, T, I>`: Revert transformation `Tr` from `T` to input `I`.
646
+ - `RevertAny<T, I>`: Revert any transformation from `T` to `I`.
647
+ - `isTransformed<T, Tr>`: Check if `T` is transformed with `Tr`.
648
+
649
+ ### Inspect Namespace
650
+
651
+ Query utilities for metadata.
652
+
653
+ - `PhantomOf<T>`: Extract full `__Phantom` object from `T`.
654
+ - `StripPhantom<T>`: Remove `__Phantom` from `T`.
655
+ - `stripPhantom(value)`: Runtime helper to strip metadata (for debugging).
656
+
657
+ Aliases for core and feature extractors: `LabelOf<T>`, `HasLabel<T, L>`, `TagOf<T>`, `HasTag<T, Ta>`, `VariantsOf<T>`, `HasVariants<T>`, `BaseOf<T>`, `HasBase<T, B>`, `InputOf<T>`, `HasInput<T>`, `TraitsOf<T>`, `TraitKeysOf<T>`, `HasTraits<T, Tr>`, `isBrand<T, B>`, `isIdentity<T, I>`, `HasTrait<T, Tr>`, `isTransformed<T, Tr>`.
658
+
659
+ ### assertors Namespace
660
+
661
+ Zero-cost casting functions.
662
+
663
+ `asBrand<B>()`: Assign brand `B`.
664
+ `asIdentity<I>()`: Assign identity `I`.
665
+ `addTrait<Tr>()`: Add trait `Tr`.
666
+ `addTraits<Tr[]>()`: Add multiple traits `Tr[]`.
667
+ `dropTrait<Tr>()`: Remove trait `Tr`.
668
+ `dropTraits<Tr[]>()`: Remove multiple traits `Tr[]`.
669
+ `applyTransformation<Tr>()`: Apply transformation `Tr` (takes `input`, `transformed`).
670
+ `revertTransformation<Tr>()`: Revert transformation `Tr` (takes `transformed`, `input`).
671
+
672
+ ### Other Utilities
673
+
674
+ `ErrorType<E>`: Unique error type for violations (e.g., `already branded`).
675
+ `PhantomChain`: Fluent class for chaining assertors (`.with(assertor)` and `.end()`).
676
+
677
+ ---
678
+
679
+ ## Full examples
680
+
681
+ ### Brand & Identity
682
+
683
+ Defining brands and identities at our app entry points after validation:
684
+
685
+ ```ts
686
+ import { Brand, assertors } from '@vicin/phantom';
687
+
688
+ declare const __UserId: unique symbol;
689
+ type UserId = Brand.Declare<typeof __UserId, 'UserId'>;
690
+ const asUserId = assertors.asBrand<UserId>();
691
+
692
+ function validateUserId(userId: string) {
693
+ const valid = userId; // replace with actual validation
694
+ return asUserId(valid);
695
+ }
696
+
697
+ // Function guarded by UserId so only return of asUserId in our validation function can be passed here
698
+ function registerUser(userId: UserId) {
699
+ // handle registring
700
+ }
701
+
702
+ const input = 'some user id';
703
+ const validUserId = validateUserId(input);
704
+ registerUser(validUserId); // no error
705
+ registerUser('Invalid user id'); // throws an error: Argument of type 'string' is not assignable to parameter of type '{ __Phantom: { __Tag: unique symbol; __Label?: "UserId"; }; }'.
706
+ ```
707
+
708
+ ---
709
+
710
+ ### Traits
711
+
712
+ Marking values with special traits, for example 'PII' to avoid logging personal information:
713
+
714
+ ```ts
715
+ import { Trait, assertors } from '@vicin/phantom';
716
+
717
+ declare const __PII: unique symbol;
718
+ type PII = Phantom.Trait.Declare<typeof __PII>;
719
+ const addPII = Phantom.assertors.addTrait<PII>();
720
+
721
+ function log<T>(value: T extends PII ? never : T) {}
722
+
723
+ const publicValue = 'some value';
724
+ const secretValue = addPII('secret value');
725
+
726
+ log(publicValue); // no error
727
+ log(secretValue); // throws an error: Argument of type '_Add<PII, "secret value">' is not assignable to parameter of type 'never'.
728
+ ```
729
+
730
+ ---
731
+
732
+ ### Transformation
733
+
734
+ Transformations shine in pipelines, typically `apply`/`revert` assertors are used inside `apply`/`revert` functions, which creates full type-safe pipe that remember type before each step:
735
+
736
+ ```ts
737
+ import { Transformation, assertors } from '@vicin/phantom';
738
+
739
+ /** --------------------------
740
+ * Encode type and functions
741
+ * -------------------------- */
742
+
743
+ declare const __Encoded: unique symbol;
744
+ type Encoded<I = unknown> = Transformation.Declare<
745
+ I,
746
+ typeof __Encoded,
747
+ 'Encoded value',
748
+ Uint8Array
749
+ >;
750
+ const applyEncode = assertors.applyTransformation<Encoded>();
751
+ const revertEncode = assertors.revertTransformation<Encoded>();
752
+
753
+ function encode<V>(value: V) {
754
+ const encoded = value as Uint8Array; // replace with actual encoding
755
+ return applyEncode(value, encoded);
756
+ }
757
+
758
+ function decode<E extends Encoded>(encoded: E) {
759
+ const decoded = encoded; // replace with actual decoding
760
+ return revertEncode(encoded, decoded);
761
+ }
762
+
763
+ /** --------------------------
764
+ * Encrypt type and functions
765
+ * -------------------------- */
766
+
767
+ declare const __Encrypted: unique symbol;
768
+ type Encrypted<I = unknown> = Transformation.Declare<
769
+ I,
770
+ typeof __Encrypted,
771
+ 'Encrypted value',
772
+ Uint8Array
773
+ >;
774
+ const applyEncrypt = assertors.applyTransformation<Encrypted>();
775
+ const revertEncrypt = assertors.revertTransformation<Encrypted>();
776
+
777
+ function encrypt<V>(value: V) {
778
+ const encrypted = value as Uint8Array; // replace with actual encryption
779
+ return applyEncrypt(value, encrypted);
780
+ }
781
+
782
+ function decrypt<E extends Encrypted>(encrypted: E) {
783
+ const decrypted = encrypted; // replace with actual decryption
784
+ return revertEncrypt(encrypted, decrypted);
785
+ }
786
+
787
+ /** --------------------------
788
+ * Usage: encode then encrypt a value
789
+ * -------------------------- */
790
+
791
+ const value = 'some value';
792
+
793
+ // apply
794
+ const encoded = encode(value);
795
+ const encrypted = encrypt(encoded);
796
+
797
+ // revert
798
+ const decrypted = decrypt(encrypted);
799
+ const orignalValue = decode(decrypted);
800
+
801
+ // if you tried to decode for example before decrypting:
802
+ const originalValue = decode(encrypted); // throws an error: Type 'typeof __Encrypted' is not assignable to type 'typeof __Encoded' in the last line.
803
+
804
+ /** --------------------------
805
+ * Usage: safe-guards
806
+ * -------------------------- */
807
+
808
+ // if we need encoded then encrypted string in our function we can just:
809
+ function save(enc: Encrypted<Encoded<string>>) {
810
+ // handle save into db here
811
+ }
812
+
813
+ save(encrypted); // no errors
814
+ save(encoded); // throws an error: Type 'string' is not assignable to type '{ __Phantom: { __Input: string; __Tag: unique symbol; __Label?: "Encoded value"; __Base?: Uint8Array<ArrayBufferLike>; }; }'.
815
+ save('some string'); // throws an error: Argument of type 'string' is not assignable to parameter of type '{ __Phantom: { __Input: { __Phantom: { __Input: string; __Tag: unique symbol; __Label?: "Encoded value"; __Base?: Uint8Array<ArrayBufferLike>; }; }; __Tag: unique symbol; __Label?: "Encrypted value"; __Base?: Uint8Array<...>; }; }'.
816
+ ```
817
+
818
+ Also it can be used in one-way transformations (e.g. `Hashed`):
819
+
820
+ ```ts
821
+ import { Transformation, assertors } from '@vicin/phantom';
822
+
823
+ declare const __Hashed: unique symbol;
824
+ type Hashed<I = unknown> = Transformation.Declare<
825
+ I,
826
+ typeof __Hashed,
827
+ 'Hashed value',
828
+ string
829
+ >;
830
+ const applyHash = assertors.applyTransformation<Hashed>();
831
+
832
+ function hash<V>(value: V) {
833
+ const hashed = value as string; // replace with actual hash
834
+ return applyHash(value, hashed);
835
+ }
836
+ ```
837
+
838
+ ---
839
+
840
+ ## Sigil
841
+
842
+ `Sigil` is another lightweight TypeScript library I created for **nominal identity on classes**, providing compile-time branding and reliable runtime type checks. It solves the unreliability of `instanceof` in monorepos, bundled/HMR environments (where classes can duplicate), and manual branding boilerplate in DDD by using `symbols`, `lineages`, and a `centralized registry` for stable identity across codebases.
843
+
844
+ `Sigil` works seamlessly in conjunction with `Phantom`,use `Phantom` for nominal typing on primitives/objects (type-only metadata), and `Sigil` for classes. Together, they enable comprehensive domain modeling: e.g., a Phantom-branded `UserId` could be a property in a Sigil-branded `User` class, combining zero-runtime primitive safety with robust class-level checks.
845
+
846
+ - GitHub: **[@sigil](https://github.com/ZiadTaha62/sigil)**
847
+ - NPM: **[@sigil](https://www.npmjs.com/package/@vicin/sigil)**
848
+
849
+ ---
850
+
851
+ ## Contributing
852
+
853
+ Any contributions you make are **greatly appreciated**.
854
+
855
+ Please see our [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
856
+
857
+ ## License
858
+
859
+ Distributed under the MIT License. See `LICENSE` for more information.
860
+
861
+ ---
862
+
863
+ ## Author
864
+
865
+ Built with ❤️ by **Ziad Taha**.
866
+
867
+ - GitHub: [@ZiadTaha62](https://github.com/ZiadTaha62)
868
+ - NPM: [@ziadtaha62](https://www.npmjs.com/~ziadtaha62)
869
+ - Vicin: [@vicin](https://www.npmjs.com/org/vicin)