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