@vicin/phantom 1.0.5 → 1.1.0

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