@vicin/phantom 1.0.4 → 1.0.8

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