@vicin/sigil 1.2.0 → 1.2.3

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,871 +1,616 @@
1
- # Sigil
2
-
3
- `Sigil` is a lightweight TypeScript library for creating **nominal identity classes** with compile-time branding and reliable runtime type checks. It organizes classes across your code and gives you power of **nominal typing**, **safe class checks across bundles** and **centralized registry** where reference to every class constructor is stored and enforced to have its own unique label and symbol.
4
-
5
- > **Key ideas:**
6
- >
7
- > - **Compile-time nominal typing** via type brands so two structurally-identical types can remain distinct.
8
- > - **Reliable runtime guards** using `Symbol.for(...)` and lineage sets instead of `instanceof`.
9
- > - **Inheritance-aware identity**: lineages and sets let you test for subtype/supertype relationships.
10
- > - **Centralized class registry**: every class have its own unique label and symbol that can be used as an id throughout the codebase.
11
-
12
- **Note: You should read these parts before implementing `Sigil` in your code:**
13
-
14
- - **Security note:** By default, `Sigil` stores constructor references in a global registry. While this does not expose private instance data, it allows any module to retrieve a class constructor via its label. If you have sensitive classes that should not be accessible globally, update your options:
15
-
16
- ```ts
17
- @WithSigil("label", { storeConstructor: false })
18
- ```
19
-
20
- See the [Registry](#registry) section for more details.
21
-
22
- - **Performance & Hot-Paths note:** `Sigil` attaches minimal metadata to instances, which is negligible for 99% of use cases. While `.isOfType()` is optimized, it is inherently slower than the native `instanceof` operator. For extreme hot-path code where every microsecond counts, stick to native checks—but keep in mind you'll lose Sigil's cross-bundle reliability.
23
-
24
- - **HOF & Private Constructors note:** Due to a known TypeScript limitation with class expressions, using HOF helpers (like `withSigilTyped`) on classes with private constructors still allow those classes to be extended in the type system. If strict constructor encapsulation is a priority, please review the [Private Constructors](#private-constructors) guide.
25
-
26
- - **Simplified instanceof Replacement:** `Sigil` relies on unique labels to identify classes. If your primary goal is simply to fix `instanceof` across bundles without the overhead of nominal typing or a registry, you can jump straight to our [Minimal Setup Guide](#i-dont-care-about-nominal-types-or-central-registry-i-just-want-a-runtime-replacement-of-instanceof).
27
-
28
- ---
29
-
30
- ## Table of contents
31
-
32
- - [Features](#features)
33
- - [Quick start](#quick-start)
34
- - [Install](#install)
35
- - [Basic usage (mixin / base class)](#basic-usage-mixin--base-class)
36
- - [Decorator style](#decorator-style)
37
- - [HOF helpers](#hof-higher-order-function-helpers)
38
- - [Minimal “first-run” example](#minimal-first-run-example)
39
- - [Migration](#migration)
40
- - [Limitations & guarantees](#limitations--guarantees)
41
- - [Core concepts](#core-concepts)
42
- - [Nominal typing](#nominal-typing)
43
- - [API reference](#api-reference)
44
- - [Options & configuration](#options--configuration)
45
- - [Registry](#registry)
46
- - [Security guidance](#security-guidance)
47
- - [Troubleshooting & FAQ](#troubleshooting--faq)
48
- - [Deprecated API](#deprecated-api)
49
- - [Phantom](#phantom)
50
- - [Contributing](#contributing)
51
- - [License](#license)
52
- - [Author](#author)
53
-
54
- ---
55
-
56
- ## Features
57
-
58
- - Attach **stable runtime identity** to classes using `Symbol.for(label)`.
59
-
60
- - **Type-level branding** so distinct domain identities are enforced by TypeScript.
61
-
62
- - **Lineage tracking** (arrays + sets of symbols) for O(1) and O(n) checks.
63
-
64
- - Easy to use: decorator (`@WithSigil`), mixin (`Sigilify`), and HOF (Higher order function) helpers (`withSigil`, `withSigilTyped`, `typed`).
65
-
66
- - **Global registry** to centralize classes (query any class by its label in run-time) and guard against duplicate labels.
67
-
68
- - Minimal runtime overhead in production (DEV checks can be toggled off).
69
-
70
- ---
71
-
72
- ## Quick start
73
-
74
- ### Install
75
-
76
- ```bash
77
- npm install @vicin/sigil
78
- # or
79
- yarn add @vicin/sigil
80
- # or
81
- pnpm add @vicin/sigil
82
- ```
83
-
84
- **Requirements**: TypeScript 5.0+ (for stage-3 decorators) and Node.js 18+ recommended. however HOF can be used in older TypeScript versions.
85
-
86
- ### Basic usage (mixin / base class)
87
-
88
- Use the `Sigil` base class or the `Sigilify` mixin to opt a class into the Sigil runtime contract.
89
-
90
- ```ts
91
- import { Sigil, Sigilify } from '@vicin/sigil';
92
-
93
- // Using the pre-sigilified base class:
94
- class User extends Sigil {}
95
-
96
- // Or use Sigilify when you want an ad-hoc class:
97
- const MyClass = Sigilify(class {}, '@myorg/mypkg.MyClass');
98
- ```
99
-
100
- This adds runtime metadata to the constructor and allows you to use runtime helpers (see API).
101
-
102
- ### Decorator style
103
-
104
- Apply a label with the `@WithSigil` decorator. This is handy for small classes or when you prefer decorator syntax.
105
-
106
- ```ts
107
- import { Sigil, WithSigil } from '@vicin/sigil';
108
-
109
- @WithSigil('@myorg/mypkg.User')
110
- class User extends Sigil {}
111
- ```
112
-
113
- > Note: When extending an already sigilized class (for example `Sigil`), you must decorate the subclass or use the HOF helpers in DEV mode unless you configured the library otherwise.
114
-
115
- ### HOF (Higher-Order Function) helpers
116
-
117
- HOFs work well in many build setups and are idempotent-safe for HMR flows.
118
-
119
- ```ts
120
- import { Sigil, withSigil } from '@vicin/sigil';
121
-
122
- class _User extends Sigil {}
123
- const User = withSigil(_User, '@myorg/mypkg.User');
124
-
125
- const user = new User();
126
- console.log(User.SigilLabel); // "@myorg/mypkg.User"
127
- ```
128
-
129
- ### Minimal “first-run” example
130
-
131
- ```ts
132
- import { Sigil, withSigil } from '@vicin/sigil';
133
-
134
- class _User extends Sigil {
135
- constructor(public name: string) {
136
- super();
137
- }
138
- }
139
- export const User = withSigil(_User, '@myorg/mypkg.User');
140
-
141
- const u = new User('alice');
142
-
143
- console.log(User.SigilLabel); // "@myorg/mypkg.User"
144
- console.log(User.isOfType(u)); // true
145
- ```
146
-
147
- ### Migration
148
-
149
- Migrating old code into `Sigil` can be done seamlessly with this set-up:
150
-
151
- 1. Set `SigilOptions.autofillLabels` to `true` at the start of the app so no errors are thrown in the migration stage:
152
-
153
- ```ts
154
- import { updateOptions } from '@vicin/sigil';
155
- updateOptions({ autofillLabels: true });
156
- ```
157
-
158
- 2. Make your base classes extends `Sigil`:
159
-
160
- ```ts
161
- import { Sigil } from '@vicin/sigil';
162
-
163
- class MyBaseClass {} // original
164
-
165
- class MyBaseClass extends Sigil {} // <-- add 'extends Sigil' here
166
- ```
167
-
168
- Just like this, your entire classes are sigilized and you can start using `.isOfType()` as a replacement of `instanceof` in cross bundle checks.
169
- But there is more to add to your system, which will be discussed in the [Core concepts](#core-concepts).
170
-
171
- ---
172
-
173
- ## Limitations & guarantees
174
-
175
- This section states clearly what `Sigil` provides and what it does **not** provide.
176
-
177
- ### What Sigil guarantees
178
-
179
- **1. Stable label symbol mapping within the same JS global symbol registry.**
180
-
181
- - If two bundles share the same **global symbol registry** (the thing `Symbol.for(...)` uses), then `Symbol.for(label)` is identical across them — enabling cross-bundle identity checks when both sides use the same label string.
182
-
183
- **2. Reliable runtime identity (when used as intended).**
184
-
185
- - When classes are sigilified and their labels are used consistently, `.isOfType()` and the `SigilTypeSet` checks produce stable results across bundles/Hot Module Replacement flows that share the same runtime/global symbol registry.
186
-
187
- **3. Optional central registry for discovery & serialization helpers.**
188
-
189
- - If enabled, the registry centralizes labels (and optionally constructor references), which can be used for label-based serialization or runtime lookups within a trusted runtime.
190
-
191
- **4. Nominal typing that is inheritance-aware**
192
-
193
- - With couple extra lines of code you can have nominally typed classes.
194
-
195
- ### What Sigil does not guarantee
196
-
197
- **2. It is not for across isolated JS realms.**
198
-
199
- Examples of isolated realms where Sigil may not work as expected:
200
-
201
- - iframe with a different global context that does not share the same window (and therefore a different symbol registry).
202
- - Workers or processes that do not share the same `globalThis` / symbol registry.
203
- - Cross-origin frames where symbols are not shared.
204
-
205
- In such cases you must provide a bridging/serialization protocol that maps labels to constructors on each side. however `Sigil` if used as intended makes serialization protocol much easier as each class will have a unique label.
206
-
207
- **3. Not a security or access-control mechanism.**
208
-
209
- Presence of a constructor or label in the registry is discoverable (unless you purposely set `storeConstructor: false`). Do **not** use `Sigil` as an authorization or secrets mechanism.
210
-
211
- ---
212
-
213
- ## Core concepts
214
-
215
- ### Terminology
216
-
217
- - **Label**: A human-readable identity (string) such as `@scope/pkg.ClassName`.
218
- - **SigilType (symbol)**: `Symbol.for(label)` stable across realms that share the global registry.
219
- - **Type lineage**: An array `[parentSymbol, childSymbol]` used for strict ancestry checks.
220
- - **Type set**: A `Set<symbol>` built from the lineage for O(1) membership checks.
221
- - **Brand**: A compile-time-only TypeScript marker carried on instances so the type system treats labelled classes nominally.
222
- - **Registry**: A global Map of registered `Sigil` classes keyed by there labels.
223
-
224
- ---
225
-
226
- ### Why `Sigil` exists
227
-
228
- `Sigil` was born out of real-world friction in a large **monorepo** built with **Domain-Driven Design (DDD)**.
229
-
230
- #### The monorepo (`instanceof`) problem
231
-
232
- The first issue surfaced with `instanceof`.
233
- In modern JavaScript setups—monorepos, multiple bundles, HMR, transpiled builds—the same class can be defined more than once at runtime. When that happens, `instanceof` becomes unreliable:
234
-
235
- - Objects created in one bundle fail `instanceof` checks in another
236
- - Hot reloads can silently break identity checks
237
- - Runtime behavior diverges from what the type system suggests
238
-
239
- This made instanceof unsuitable as a foundation for domain identity.
240
-
241
- #### The DDD (`Manual branding`) problem
242
-
243
- We started embedding custom identifiers directly into class to achieve nominal typing.
244
- While this worked conceptually, it quickly became problematic:
245
-
246
- - Every class needed boilerplate fields or symbols
247
- - Type guards had to be hand-written and maintained
248
- - Inheritance required extra care to preserve identity
249
-
250
- The intent of the domain model was obscured by repetitive code, What started as a workaround became verbose, fragile, and hard to enforce consistently.
251
-
252
- #### A better abstraction
253
-
254
- Sigil is the result of abstracting that pattern into a **first-class identity system**:
255
-
256
- - **Nominal identity at compile time**, without structural leakage
257
- - **Reliable runtime type checks**, without instanceof
258
- - **Inheritance-aware identity**, with lineage tracking
259
- - **Minimal runtime overhead**, with DEV-only safeguards
260
-
261
- Instead of embedding identity logic inside every class, Sigil centralizes it, enforces it, and makes it explicit.
262
-
263
- The goal is simple:
264
-
265
- - **Make domain identity correct by default, and hard to get wrong.**
266
-
267
- ---
268
-
269
- ### How Sigil solves the problems
270
-
271
- #### Problem A — `instanceof` is unreliable
272
-
273
- To make runtime identity reliable across bundles, HMR, and transpiled code, `Sigil` explicitly attaches identity metadata and tracks inheritance lineage on classes. That tracking starts with a contract created by `Sigilify()` (a mixin) or by extending the `Sigil` base class. Sigilify augments a plain JS class with the metadata and helper methods Sigil needs (for example, `isOfType`).
274
-
275
- Basic patterns:
276
-
277
- **Mixin:**
278
-
279
- ```ts
280
- import { Sigilify } from '@vicin/sigil';
281
-
282
- const MyClass = Sigilify(class {}, 'MyClass');
283
- ```
284
-
285
- **Direct base-class extend:**
286
-
287
- ```ts
288
- import { Sigil, WithSigil } from '@vicin/sigil';
289
-
290
- @WithSigil('MyClass')
291
- class MyClass extends Sigil {}
292
- ```
293
-
294
- Once you opt into the runtime contract, Sigil enforces consistency: in DEV mode, extending a sigil-aware class without using decorator `@WithSigil` or a provided HOF (e.g. `withSigil` or `withSigilTyped`) will throw a helpful error. If you prefer a laxer setup, Sigil can be configured to auto-label.
295
-
296
- **Decorator:**
297
-
298
- ```ts
299
- import { Sigil, WithSigil } from '@vicin/sigil';
300
-
301
- @WithSigil('MyClass')
302
- class MyClass extends Sigil {}
303
- ```
304
-
305
- **HOF:**
306
-
307
- ```ts
308
- import { Sigil, withSigil } from '@vicin/sigil';
309
-
310
- class _MyClass extends Sigil {}
311
- const MyClass = withSigil(_MyClass, 'MyClass');
312
- ```
313
-
314
- #### Problem B — Branding can get messy
315
-
316
- Runtime metadata alone does not change TypeScript types. To get compile-time nominal typing (so `UserId` ≠ `PostId` even with the same shape), Sigil provides two patterns:
317
-
318
- **Decorator with brand field:**
319
-
320
- ```ts
321
- import { Sigil, WithSigil, UpdateSigilBrand } from '@vicin/sigil';
322
-
323
- @WithSigil('User')
324
- class User extends Sigil {
325
- declare __SIGIL_BRAND__: UpdateSigilBrand<'User', Sigil>; // <-- inject type
326
- }
327
- ```
328
-
329
- **HOF with `_Class` / `Class`:**
330
-
331
- ```ts
332
- import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
333
-
334
- // Untyped (runtime) base you extend as normal TS class code:
335
- class _User extends Sigil {}
336
-
337
- // Create a fully typed & runtime-safe class:
338
- const User = withSigilTyped(_User, 'User');
339
- type User = GetInstance<typeof User>;
340
- ```
341
-
342
- Typings are lineage aware as well:
343
-
344
- ```ts
345
- import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
346
-
347
- class _A extends Sigil {}
348
- const A = withSigilTyped(_A, 'A');
349
- type A = GetInstance<typeof A>;
350
-
351
- class _B extends A {}
352
- const B = withSigilTyped(_B, 'B');
353
- type B = GetInstance<typeof B>;
354
-
355
- type test1 = A extends B ? true : false; // false
356
- type test2 = B extends A ? true : false; // true
357
- ```
358
-
359
- #### SigilLabel & SigilType
360
-
361
- If library is used with default options, `SigilLabel` & `SigilType` are **100% unique** for each class, which make them perfect replacement of manual labeling across your code that comes shipped with Sigil by default. you can access them in class constructor directly of via `getSigilLabel()` and `getSigilType()` in instances.
362
-
363
- ---
364
-
365
- ## Nominal typing
366
-
367
- In this part we will discuss conventions to avoid any type errors and have normal developing experience with just extra few definition lines at the bottom of the file.
368
- First we have two patterns, **HOF with `_Class` / `Class`** and **Decorators with brand field**:
369
-
370
- ### HOF with `_Class` / `Class`
371
-
372
- The update of `Sigil` brand types happens via HOF that are defined below actual class definition:
373
-
374
- ```ts
375
- import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
376
-
377
- class _X extends Sigil {
378
- // All logic for class
379
- }
380
-
381
- export const X = withSigilTyped(_X, 'Label.X'); // <-- Pass class with label to uniquely identify it from other classes
382
- export type X = GetInstance<typeof X>;
383
- ```
384
-
385
- In other parts of the code:
386
-
387
- ```ts
388
- import { X } from './example.ts';
389
-
390
- class _Y extends X {
391
- // All logic as earlier
392
- }
393
-
394
- export const Y = withSigilTyped(_Y, 'Label.Y');
395
- export type Y = GetInstance<typeof Y>;
396
- ```
397
-
398
- So as we have seen nominal identity is introduced with few lines only below each class. and the bulk code where logic lives is untouched.
399
-
400
- #### `InstanceType<>` vs `GetInstance<>`
401
-
402
- You should depend on `GetInstance` to get type of instance and avoid using `InstanceType` as it returns `any` if the class constructor is `protected` or `private`.
403
-
404
- ```ts
405
- import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
406
-
407
- class _X extends Sigil {}
408
-
409
- export const X = withSigilTyped(_X, 'Label.X');
410
- export type X = GetInstance<typeof X>; // <-- works with 'private' and 'protected' constructors as well
411
- ```
412
-
413
- Internally `GetInstance` is just `T extends { prototype: infer R }`.
414
-
415
- #### Generic propagation
416
-
417
- One of the downsides of defining typed class at the bottom is that we need to redefine generics as well in the type.
418
-
419
- Example of generic propagation:
420
-
421
- ```ts
422
- import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
423
-
424
- // Untyped base classes used for implementation:
425
- class _X<G> extends Sigil {}
426
-
427
- export const X = withSigilTyped(_X, 'Label.X');
428
- export type X<G> = GetInstance<typeof X<G>>; // <-- Generics re-defined here, just copy-paste and pass them
429
- ```
430
-
431
- #### Anonymous classes
432
-
433
- You may see error: `Property 'x' of exported anonymous class type may not be private or protected.`, although this is rare to occur.
434
- This comes from the fact that all typed classes are `anonymous class` as they are return of HOF and ts compiler struggle to type them safely. to avoid these error entirely all you need is exporting the untyped classes even if they are un-used as a good convention.
435
-
436
- ```ts
437
- import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
438
-
439
- export class _X extends Sigil {} // <-- Just add 'export' here
440
-
441
- export const X = withSigilTyped(_X, 'Label.X');
442
- export type X = GetInstance<typeof X>;
443
- ```
444
-
445
- #### Private constructors
446
-
447
- The only limitation in HOF approach is **extending private constructors**:
448
-
449
- ```ts
450
- import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
451
- class _X extends Sigil {
452
- private constructor() {}
453
- }
454
- const X = withSigilTyped(_X, 'X');
455
- type X = GetInstance<typeof X>;
456
-
457
- class _Y extends X {} // <-- This is allowed!
458
- const Y = withSigilTyped(_Y, 'Y');
459
- type Y = GetInstance<typeof Y>;
460
-
461
- const y = new Y(); // <-- Type here is any
462
- ```
463
-
464
- Unfortunately this is a limitation in typescript and i couldn't find any solution to adress it.
465
-
466
- ---
467
-
468
- ### Decorators with brand field
469
-
470
- The update of `Sigil` brand type happens directly by overriding `__SIGIL_BRAND__` field:
471
-
472
- ```ts
473
- import { Sigil, WithSigil, UpdateSigilBrand } from '@vicin/sigil';
474
-
475
- @WithSigil('X')
476
- class X extends Sigil {
477
- declare __SIGIL_BRAND__: UpdateSigilBrand<'X', Sigil>;
478
- }
479
-
480
- @WithSigil('Y')
481
- class Y extends X {
482
- declare __SIGIL_BRAND__: UpdateSigilBrand<'Y', Sigil>;
483
- }
484
- ```
485
-
486
- As you see no `_Class`/`Class` pattern, no `private constructor` issue and no type hacks, but our branding logic now lives in class body.
487
-
488
- To be more explicit and prevent mismatch between run-time and compile-time labels we can:
489
-
490
- ```ts
491
- import { Sigil, WithSigil, UpdateSigilBrand } from '@vicin/sigil';
492
-
493
- const label = 'X';
494
- @WithSigil(label)
495
- class X extends Sigil {
496
- declare __SIGIL_BRAND__: UpdateSigilBrand<typeof label, Sigil>;
497
- }
498
- ```
499
-
500
- ---
501
-
502
- ## API reference
503
-
504
- > The runtime API is intentionally small and focused. Types are exported for consumers that want to interact with compile-time helpers.
505
-
506
- ### Exports
507
-
508
- Top-level exports from the library:
509
-
510
- ```ts
511
- export { Sigil, SigilError } from './classes';
512
- export { WithSigil } from './decorator';
513
- export { withSigil, withSigilTyped } from './enhancers';
514
- export {
515
- isDecorated,
516
- isInheritanceChecked,
517
- isSigilBaseCtor,
518
- isSigilBaseInstance,
519
- isSigilCtor,
520
- isSigilInstance,
521
- } from './helpers';
522
- export { Sigilify } from './mixin';
523
- export {
524
- updateOptions,
525
- SigilRegistry,
526
- getActiveRegistry,
527
- DEFAULT_LABEL_REGEX,
528
- } from './options';
529
- export type {
530
- ISigil,
531
- TypedSigil,
532
- GetInstance,
533
- SigilBrandOf,
534
- SigilOptions,
535
- UpdateSigilBrand,
536
- } from './types';
537
- ```
538
-
539
- ### Key helpers (runtime)
540
-
541
- - `Sigil`: a minimal sigilified base class you can extend from.
542
- - `SigilError`: an `Error` class decorated with a sigil so it can be identified at runtime.
543
- - `WithSigil(label)`: class decorator that attaches sigil metadata at declaration time.
544
- - `Sigilify(Base, label?, opts?)`: mixin function that returns a new constructor with sigil types and instance helpers.
545
- - `withSigil(Class, label?, opts?)`: HOF that validates and decorates an existing class constructor.
546
- - `withSigilTyped(Class, label?, opts?)`: like `withSigil` but narrows the TypeScript type to include brands.
547
- - `isSigilCtor(value)`: `true` if `value` is a sigil constructor.
548
- - `isSigilInstance(value)`: `true` if `value` is an instance of a sigil constructor.
549
- - `SigilRegistry`: `Sigil` Registy class used to centralize classes across app.
550
- - `getActiveRegistry`: Getter of active registry being used by `Sigil`.
551
- - `updateOptions(opts, mergeRegistries?)`: change global runtime options before sigil decoration (e.g., `autofillLabels`, `devMarker`, etc.).
552
- - `DEFAULT_LABEL_REGEX`: regex that ensures structure of `@scope/package.ClassName` to all labels, it's advised to use it as your `SigilOptions.labelValidation`
553
-
554
- ### Instance & static helpers provided by Sigilified constructors
555
-
556
- When a constructor is decorated/sigilified it will expose the following **static** getters/methods:
557
-
558
- - `SigilLabel` — the human label string.
559
- - `SigilType` the runtime symbol for the label.
560
- - `SigilTypeLineage` — readonly array of symbols representing parent → child.
561
- - `SigilTypeSet` — readonly `Set<symbol>` for O(1) checks.
562
- - `isSigilified(obj)` runtime predicate that delegates to `isSigilInstance`.
563
- - `isOfType(other)` — O(1) membership test using `other`'s `__TYPE_SET__`.
564
- - `isOfTypeStrict(other)` — strict lineage comparison element-by-element.
565
-
566
- Instances of sigilified classes expose instance helpers:
567
-
568
- - `getSigilLabel()` — returns the human label.
569
- - `getSigilType()` — runtime symbol.
570
- - `getSigilTypeLineage()` — returns lineage array.
571
- - `getSigilTypeSet()` returns readonly Set.
572
-
573
- ---
574
-
575
- ## Options & configuration
576
-
577
- Sigil exposes a small set of runtime options that control registry and DEV behavior. These can be modified at app startup via `updateOptions(...)` to set global options:
578
-
579
- ```ts
580
- import { updateOptions, SigilRegistry } from '@vicin/sigil';
581
-
582
- // Values defined in this example are defaults:
583
-
584
- updateOptions({
585
- autofillLabels: false, // auto-generate labels for subclasses that would otherwise inherit
586
- skipLabelInheritanceCheck: false, // skip DEV-only inheritance checks -- ALMOST NEVER WANT TO SET THIS TO TRUE, Use 'autofillLabels: true' instead. --
587
- labelValidation: null, // or a RegExp / function to validate labels
588
- devMarker: process.env.NODE_ENV !== 'production', // boolean used to block dev only checks in non-dev environments
589
- registry: new SigilRegistry(), // setting active registry used by 'Sigil'
590
- useGlobalRegistry: true, // append registry into 'globalThis' to ensure single source in the runtime in cross bundles.
591
- storeConstructor: true, // store reference of the constructor in registry
592
- });
593
- ```
594
-
595
- Global options can be overridden per class by `opts` field in decorator and HOF.
596
-
597
- ---
598
-
599
- ## Registry
600
-
601
- `Sigil`'s default options require developers to label every class, that allows central class registry that stores a reference for every class keyed by its label, also it prevent two classes in the codebase from having the same `SigilLabel`.
602
-
603
- This is mainly useful in large codebases or frameworks where they need central registry or if you need class transport across API, workers, etc... where you can use `SigilLabel` reliably to serialize class identity. to interact with registry `Sigil` exposes `getActiveRegistry` and `SigilRegistry` class. also you can update registry related options with `updateOptions`.
604
-
605
- By default, registry is stored in `globalThis` under `Symbol.for(__SIGIL_REGISTRY__)` so one instance is used across runtime even with multiple bundles, but this also exposes that map anywhere in the code, see [globalThis and security](#globalthis-and-security).
606
-
607
- ### Get registry
608
-
609
- You can interact with registry using `getActiveRegistry`, this function returns registry currently in use:
610
-
611
- ```ts
612
- import { getActiveRegistry } from '@vicin/sigil';
613
- const registry = getActiveRegistry();
614
- if (registry) console.log(registry.listLabels()); // check for presence as it can be 'null' if 'updateOptions({ registry: null })' is used
615
- ```
616
-
617
- ### Replace registry
618
-
619
- In most cases you don't need to replace registry, but if you wanted to define a `Map` and make `Sigil` use it aa a register (e.g. define custom side effects) you can use `SigilRegistry`:
620
-
621
- ```ts
622
- import { SigilRegistry, updateOptions } from '@vicin/sigil';
623
-
624
- const myMap = new Map();
625
- const myRegistry = new SigilRegistry(myMap);
626
- updateOptions({ registry: myRegistry });
627
-
628
- // Now 'Sigil' register new labels and constructors to 'myMap'.
629
- ```
630
-
631
- By default `Sigil` will merge old registry map into `myMap`, to prevent this behavior:
632
-
633
- ```ts
634
- updateOptions({ registry: myRegistry }, false); // <-- add false here
635
- ```
636
-
637
- Also you can set registry to `null`, but this is not advised as it disable all registry operations entirely:
638
-
639
- ```ts
640
- import { updateOptions } from '@vicin/sigil';
641
- updateOptions({ registry: null }); // No label checks and registry map is freed from memory
642
- ```
643
-
644
- ### globalThis and security
645
-
646
- By default registry is stored in `globalThis`. to disable this behavior you can:
647
-
648
- ```ts
649
- import { updateOptions } from '@vicin/sigil';
650
- updateOptions({ useGlobalRegistry: false });
651
- ```
652
-
653
- Before applying this change, for registry to function normally you should ensure that `Sigil` is not bundles twice in your app.
654
- however if you can't ensure that only bundle of `Sigil` is used and don't want class constructors to be accessible globally do this:
655
-
656
- ```ts
657
- import { updateOptions } from '@vicin/sigil';
658
- updateOptions({ storeConstructor: false });
659
- ```
660
-
661
- Now registry only stores label of this classes and all class constructors are in the map are replaced with `null`.
662
- If you need even more control and like the global registry for classes but want to obscure only some of your classes you can pass this option per class and keep global options as is:
663
-
664
- ```ts
665
- import { withSigil, Sigil } from '@vicin/sigil';
666
-
667
- class _X extends Sigil {}
668
- const X = withSigil(_X, 'X', { storeConstructor: false });
669
- ```
670
-
671
- Pick whatever pattern you like!
672
-
673
- ### Class typing in registry
674
-
675
- Unfortunately concrete types of classes is not supported and all classes are stored as `ISigil` type. if you want concrete typings you can wrap registry:
676
-
677
- ```ts
678
- import { getActiveRegistry } from '@vicin/sigil';
679
- import { MySigilClass1 } from './file1';
680
- import { MySigilClass2 } from './file2';
681
-
682
- interface MyClasses {
683
- MySigilClass1: typeof MySigilClass1;
684
- MySigilClass2: typeof MySigilClass2;
685
- }
686
-
687
- class MySigilRegistry {
688
- listLabels(): (keyof MyClasses)[] {
689
- return getActiveRegistry()?.listLabels();
690
- }
691
- has(label: string): boolean {
692
- return getActiveRegistry()?.has(label);
693
- }
694
- get<L extends keyof MyClasses>(label: L): MyClasses[L] {
695
- return getActiveRegistry()?.get(label) as any;
696
- }
697
- unregister(label: string): boolean {
698
- return getActiveRegistry()?.unregister(label);
699
- }
700
- clear(): void {
701
- getActiveRegistry()?.clear();
702
- }
703
- replaceRegistry(newRegistry: Map<string, ISigil> | null): void {
704
- getActiveRegistry()?.replaceRegistry(newRegistry);
705
- }
706
- get size(): number {
707
- return getActiveRegistry()?.size;
708
- }
709
- }
710
-
711
- export const MY_SIGIL_REGISTRY = new MySigilRegistry();
712
- ```
713
-
714
- Now you have fully typed central class registry!
715
-
716
- ### I don't care about nominal types or central registry, i just want a runtime replacement of 'instanceof'
717
-
718
- You can run this at the start of your app:
719
-
720
- ```ts
721
- import { updateOptions } from '@vicin/sigil';
722
- updateOptions({ autofillLabels: true, storeConstructor: false });
723
- ```
724
-
725
- now you can omit all `HOF`, `Decorators` and make `Sigil` work in the background:
726
-
727
- ```ts
728
- import { Sigil } from '@vicin/sigil';
729
-
730
- class X extends Sigil {}
731
- class Y extends X {}
732
- class Z extends Y {}
733
-
734
- Z.isOfType(new Y()); // true
735
- Z.isOfType(new X()); // true
736
- Y.isOfType(new Y()); // false
737
- ```
738
-
739
- No class constructors are stored globally and no code overhead, moreover if you can ensure that `Sigil` is not bundles twice you can disable `useGlobalRegistry` and no trace of sigil in `globalThis`.
740
-
741
- ---
742
-
743
- ## Security guidance
744
-
745
- ### Recommended defaults & quick rules
746
-
747
- - Default recommendation for public/shared runtimes (web pages, untrusted workers, serverless):
748
-
749
- ```ts
750
- updateOptions({ useGlobalRegistry: false, storeConstructor: false });
751
- ```
752
-
753
- This prevents constructors from being put on `globalThis` and prevents constructors from being stored in the registry map (labels remain, but constructors are `null`).
754
-
755
- - For private server runtimes (single controlled Node process) where a central registry is desired:
756
-
757
- ```ts
758
- updateOptions({ useGlobalRegistry: true, storeConstructor: true });
759
- ```
760
-
761
- Only enable this if you control all bundles and trust the runtime environment.
762
-
763
- ### Per-class sensitivity control
764
-
765
- If you want the registry in general but need to hide particular classes (e.g., internal or security-sensitive classes), pass `storeConstructor: false` for those classes:
766
-
767
- ```ts
768
- class _Sensitive extends Sigil {}
769
- export const Sensitive = withSigil(_Sensitive, '@myorg/internal.Sensitive', {
770
- storeConstructor: false, // label kept, constructor not stored
771
- });
772
- ```
773
-
774
- This keeps the declarative identity but avoids exposing the constructor reference in the registry.
775
-
776
- ### Short warnings (do not rely on Sigil for)
777
-
778
- - **Not a security boundary:** Registry labels/constructors are discovery metadata — do not put secrets or private instance data in them or rely on them for access control.
779
-
780
- - **Third-party code can access the registry if `useGlobalRegistry: true`** — only enable that in fully trusted runtimes.
781
-
782
- ---
783
-
784
- ## Troubleshooting & FAQ
785
-
786
- **Q: My `instanceof` checks fail across bundles — will Sigil fix this?**
787
-
788
- A: Yes. Sigil uses `Symbol.for(label)` and runtime `SigilTypeSet` membership checks to provide stable identity tests that work across bundles/realms that share the global symbol registry.
789
-
790
- **Q: I accidentally extended a sigilized class without decorating the subclass; I see an error in DEV. How should I fix it?**
791
-
792
- A: Use `@WithSigil("@your/label")`, or wrap the subclass with `withSigil` / `withSigilTyped`. Alternatively, you can relax DEV checks using `updateOptions({ skipLabelInheritanceCheck: true })` but be cautious — this weakens guarantees.
793
-
794
- **Q: I got this error: 'Property 'x' of exported anonymous class type may not be private or protected.', How to fix it?**
795
-
796
- A: This error comes from the fact that all typed classes (return from `withSigil`, `withSigilTyped` or `typed`) are 'anonymous class' as they are the return of HOF. all you need to do is to export untyped classes (`_Class`) that have private or protected properties. or even export all untyped classes as a good convention even if they are not used.
797
-
798
- **Q: How do I inspect currently registered labels?**
799
-
800
- A: Use `getActiveRegistry()?.listLabels()` to get an array of registered labels.
801
-
802
- **Q: What if i want to omit labeling in some classes while enforce others?**
803
-
804
- A: You can set `SigilOptions.autofillLabels` to `true`. or if you more strict enviroment you can define empty `@WithSigil()` decorator above classes you don't care about labeling and `Sigil` will generate random label for it, but still throw if you forgot to use a decorator or HOF on a class.
805
-
806
- ---
807
-
808
- ## Deprecated API
809
-
810
- ### REGISTRY
811
-
812
- `Sigil` have moved from static reference registry to dynamic access and updates, now devs can create `SigilRegistry` class and pass it to `SigilOptions` to be be used by the library internals. however change is done gracefully and `REGISTRY` is still supported with no change in behavior but it's **marked with `deprecated` and will be removed in v2.0.0**.
813
-
814
- ```ts
815
- import { REGISTRY, getActiveRegistry } from '@vicin/sigil';
816
-
817
- // from:
818
- const present = REGISTRY.has('label');
819
-
820
- // to:
821
- const present = getActiveRegistry()?.has('label'); // Active registy can be 'null' if 'SigilOptions.registy' is set to null so we used the '?' mark
822
- ```
823
-
824
- ```ts
825
- import { REGISTRY, updateOptions, SigilRegistry } from '@vicin/sigil';
826
-
827
- // from:
828
- const newRegistry = new Map();
829
- REGISTRY.replaceRegistry(newRegistry);
830
-
831
- // to
832
- const newRegistry = new SigilRegistry(); // can pass external map to constructor if needed.
833
- updateOptions({ registry: newRegistry });
834
- ```
835
-
836
- ### typed
837
-
838
- Typed was added to add types to output from `Sigilify` mixin, but now mixin do this by default.
839
-
840
- ---
841
-
842
- ## Phantom
843
-
844
- `Phantom` is another lightweight TypeScript library I created for achieving **nominal typing** on primitives and objects through type-only metadata. It solves the problem of structural typing in TypeScript allowing accidental misuse of identical shapes (e.g., confusing `UserId` and `PostId` as both strings) by enabling compile-time distinctions with features like **brands**, **constrained identities**, **variants for states**, **additive traits**, and **reversible transformations**. This makes it ideal for domain-driven design (DDD) without runtime overhead.
845
-
846
- `Phantom` works seamlessly in conjunction with `Sigil`, use `Sigil` for nominal identity on classes (runtime-safe checks across bundles), and `Phantom` for primitives/objects. Together, they provide **end-to-end type safety**: e.g., a Sigil-branded `User` class could hold a Phantom-branded `UserId` string property, enforcing domain boundaries at both compile and runtime.
847
-
848
- - **GitHub: [@phantom](https://github.com/ZiadTaha62/phantom)**
849
- - **NPM: [@phantom](https://www.npmjs.com/package/@vicin/phantom)**
850
-
851
- ---
852
-
853
- ## Contributing
854
-
855
- Any contributions you make are **greatly appreciated**.
856
-
857
- Please see our [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
858
-
859
- ## License
860
-
861
- Distributed under the MIT License. See `LICENSE` for more information.
862
-
863
- ---
864
-
865
- ## Author
866
-
867
- Built with ❤️ by **Ziad Taha**.
868
-
869
- - **GitHub: [@ZiadTaha62](https://github.com/ZiadTaha62)**
870
- - **NPM: [@ziadtaha62](https://www.npmjs.com/~ziadtaha62)**
871
- - **Vicin: [@vicin](https://www.npmjs.com/org/vicin)**
1
+ # Sigil
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@vicin/sigil.svg)](https://www.npmjs.com/package/@vicin/sigil) [![npm downloads](https://img.shields.io/npm/dm/@vicin/sigil.svg)](https://www.npmjs.com/package/@vicin/sigil) [![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/sigil/actions/workflows/ci.yml/badge.svg)](https://github.com/ZiadTaha62/sigil/actions/workflows/ci.yml)
4
+
5
+ > 🎉 First stable release — v1.2! Happy coding! 😄💻🚀
6
+ > 📄 **Changelog:** [CHANGELOG.md](./CHANGELOG.md)
7
+
8
+ `Sigil` is a lightweight TypeScript library for creating nominal identity classes with compile-time branding and reliable runtime type checks. It organizes class identities across your codebase and gives you the power of **nominal typing**, **safe cross-bundle class checks**, and a **central registry** where each class constructor is stored under a unique label.
9
+
10
+ > **Key ideas:**
11
+ >
12
+ > - **Nominal Typing at Compile Time:** Distinguishes structurally similar types (e.g., UserId vs. PostId).
13
+ > - **Reliable Runtime Checks:** Uses symbols instead of instanceof for cross-bundle reliability.
14
+ > - **Inheritance Awareness:** Tracks lineages for subtype/supertype checks.
15
+ > - **Central Registry:** Stores class references by unique labels for easy lookup.
16
+
17
+ ## Important Notes Before Using
18
+
19
+ - **Security:** The global registry stores constructors by default, which could expose them. Disable with `{ storeConstructor: false }` for sensitive classes.
20
+ - **Performance:** Minimal overhead, but `.isOfType()` is slower than native `instanceof`. Avoid in ultra-hot paths.
21
+ - **Private Constructors:** HOF pattern allows extending private constructors in types (TypeScript limitation).
22
+ - **Simple instanceof Fix:** If you just need runtime checks without extras, see the [minimal mode](#minimal-mode) in Registry section.
23
+
24
+ ---
25
+
26
+ ## Table of contents
27
+
28
+ - [Quick start](#quick-start)
29
+ - [Install](#install)
30
+ - [Basic usage](#basic-usage)
31
+ - [Decorator pattern](#decorator-pattern)
32
+ - [HOF pattern](#hof-higher-order-function-pattern)
33
+ - [Minimal “first-run” example](#minimal-first-run-example)
34
+ - [Migration](#migration)
35
+ - [Limitations & guarantees](#limitations--guarantees)
36
+ - [What Sigil guarantees](#what-sigil-guarantees)
37
+ - [What Sigil does not guarantee](#what-sigil-does-not-guarantee)
38
+ - [Core concepts](#core-concepts)
39
+ - [Terminology](#terminology)
40
+ - [Purpose and Origins](#purpose-and-origins)
41
+ - [Implementation Mechanics](#implementation-mechanics)
42
+ - [Nominal typing patterns](#nominal-typing-patterns)
43
+ - [HOF pattern](#1-hof-pattern-_classclass)
44
+ - [Decorator pattern](#2-decorator-pattern)
45
+ - [API reference](#api-reference)
46
+ - [Options & configuration](#options--configuration)
47
+ - [Registry](#registry)
48
+ - [Security guidance](#security-guidance)
49
+ - [Minimal mode](#minimal-mode)
50
+ - [Troubleshooting & FAQ](#troubleshooting--faq)
51
+ - [Deprecated API](#deprecated-api)
52
+ - [Phantom](#phantom)
53
+ - [Contributing](#contributing)
54
+ - [License](#license)
55
+ - [Author](#author)
56
+
57
+ ---
58
+
59
+ ## Quick start
60
+
61
+ ### Install
62
+
63
+ ```bash
64
+ npm install @vicin/sigil
65
+ # or
66
+ yarn add @vicin/sigil
67
+ # or
68
+ pnpm add @vicin/sigil
69
+ ```
70
+
71
+ Requires TypeScript 5.0+ for decorators; HOFs work on older versions. Node.js 18+ recommended.
72
+
73
+ ### Basic usage
74
+
75
+ #### Opt into `Sigil`
76
+
77
+ Use the `Sigil` base class or the `Sigilify` mixin to opt a class into the Sigil runtime contract.
78
+
79
+ ```ts
80
+ import { Sigil, Sigilify } from '@vicin/sigil';
81
+
82
+ // Using the pre-sigilified base class:
83
+ class User extends Sigil {}
84
+
85
+ // Or use Sigilify when you want an ad-hoc class:
86
+ const MyClass = Sigilify(class {}, '@myorg/mypkg.MyClass');
87
+ ```
88
+
89
+ This adds runtime metadata to the constructor and allows you to use runtime helpers, see [API reference](#api-reference).
90
+
91
+ #### Extend `Sigil` classes
92
+
93
+ After opting into the `Sigil` contract, labels are passed to child classes to uniquely identify them, they can be supplied using two patterns:
94
+
95
+ ##### Decorator pattern
96
+
97
+ Apply a label with the `@WithSigil` decorator. This is handy for small classes or when you prefer decorator syntax.
98
+
99
+ ```ts
100
+ import { Sigil, WithSigil } from '@vicin/sigil';
101
+
102
+ @WithSigil('@myorg/mypkg.User')
103
+ class User extends Sigil {}
104
+ ```
105
+
106
+ > Note: When extending an already sigilified class (for example `Sigil`), you must decorate the subclass or use the HOF helpers in DEV mode unless you configured the library otherwise.
107
+
108
+ ##### HOF (Higher-Order Function) pattern
109
+
110
+ HOFs work well in many build setups and are idempotent-safe for HMR flows.
111
+
112
+ ```ts
113
+ import { Sigil, withSigil } from '@vicin/sigil';
114
+
115
+ class _User extends Sigil {}
116
+ const User = withSigil(_User, '@myorg/mypkg.User');
117
+
118
+ const user = new User();
119
+ console.log(User.SigilLabel); // "@myorg/mypkg.User"
120
+ ```
121
+
122
+ ### Minimal “first-run” example
123
+
124
+ ```ts
125
+ import { Sigil, withSigil } from '@vicin/sigil';
126
+
127
+ class _User extends Sigil {
128
+ constructor(public name: string) {
129
+ super();
130
+ }
131
+ }
132
+ export const User = withSigil(_User, '@myorg/mypkg.User');
133
+
134
+ const u = new User('alice');
135
+
136
+ console.log(User.SigilLabel); // "@myorg/mypkg.User"
137
+ console.log(User.isOfType(u)); // true
138
+ ```
139
+
140
+ ### Migration
141
+
142
+ Migrating old code into `Sigil` can be done seamlessly with this set-up:
143
+
144
+ 1. Set `SigilOptions.autofillLabels` to `true` at the start of the app so no errors are thrown in the migration stage:
145
+
146
+ ```ts
147
+ import { updateOptions } from '@vicin/sigil';
148
+ updateOptions({ autofillLabels: true });
149
+ ```
150
+
151
+ 2. Pass your base class to `Sigilify` mixin:
152
+
153
+ ```ts
154
+ import { Sigilify } from '@vicin/sigil';
155
+
156
+ const MySigilBaseClass = Sigilify(MyBaseClass);
157
+ ```
158
+
159
+ 3. Or extend it with `Sigil`:
160
+
161
+ ```ts
162
+ import { Sigil } from '@vicin/sigil';
163
+
164
+ class MyBaseClass extends Sigil {} // <-- add 'extends Sigil' here
165
+ ```
166
+
167
+ Congratulations — you’ve opted into `Sigil` and you can start replacing `instanceof` with `isOfType`, however there is more to add to your system, check [Core concepts](#core-concepts) for more.
168
+
169
+ ---
170
+
171
+ ## Limitations & guarantees
172
+
173
+ This section states clearly what `Sigil` provides and what it does **not** provide.
174
+
175
+ ### What Sigil guarantees
176
+
177
+ **1. Stable label → symbol mapping within the same JS global symbol registry.**
178
+
179
+ **2. Reliable runtime identity (when used as intended).**
180
+
181
+ **3. Optional central registry for discovery & serialization helpers.**
182
+
183
+ **4. Nominal typing that is inheritance-aware**
184
+
185
+ ### What Sigil does not guarantee
186
+
187
+ **1. Doesn't work across isolated realms (e.g., iframes, workers) without custom bridging.**
188
+
189
+ **2. Not for security/access control constructors can be discoverable.**
190
+
191
+ ---
192
+
193
+ ## Core concepts
194
+
195
+ ### Terminology
196
+
197
+ - **Label**: A human-readable identity (string) such as `@scope/pkg.ClassName`.
198
+ - **SigilType (symbol)**: `Symbol.for(label)` — for runtime stability.
199
+ - **Type lineage**: Array of symbols for ancestry.
200
+ - **Type set**: Set of symbols for fast checks.
201
+ - **Brand**: TypeScript marker (`__SIGIL_BRAND__`) for nominal types.
202
+ - **Registry**: A global Map of registered `Sigil` classes keyed by their labels.
203
+
204
+ ---
205
+
206
+ ### Purpose and Origins
207
+
208
+ Sigil addresses issues in large monorepos and Domain-Driven Design (DDD):
209
+
210
+ - **Unreliable `instanceof`:** Bundling and HMR cause class redefinitions, breaking checks.
211
+ - **Manual Branding Overhead:** Custom identifiers lead to boilerplate and maintenance issues.
212
+
213
+ `Sigil` abstracts these into a **centralized system**, making identity management **explicit** and **error-resistant**.
214
+
215
+ ### Implementation Mechanics
216
+
217
+ - **Runtime Contract:** Established via extending `Sigil` or using `Sigilify` mixin.
218
+ - **Update metadata:** With each new child, HOF or decorators are used to attach metadata and update nominal type.
219
+ - **Accessors & Type guards:** Classes expose `SigilLabel`, `SigilType`; instances provide `getSigilLabel()` and `getSigilType()` for querying unique identifier label or symbol. also when typed it hold nominal identity used to prevent subtle bugs.
220
+
221
+ ```ts
222
+ import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
223
+
224
+ // Runtime contract
225
+ class _MyClass extends Sigil {}
226
+
227
+ // Update metadata (append new label)
228
+ const MyClass = withSigilTyped(_MyClass, '@scope/package.MyClass');
229
+ type MyClass = GetInstance<typeof MyClass>;
230
+
231
+ // Accessors & Type guards
232
+ console.log(MyClass.SigilLabel); // '@scope/package.MyClass'
233
+ console.log(new MyClass().getSigilType()); // Symbol.for('@scope/package.MyClass')
234
+ console.log(MyClass.isOfType(new MyClass())); // true
235
+ function x(c: MyClass) {} // Only instances created by 'MyClass' can be passed
236
+ ```
237
+
238
+ ---
239
+
240
+ ## Nominal typing patterns
241
+
242
+ In this part we will discuss conventions to avoid any type errors and have nominal typing with just extra few definition lines.
243
+ We have two patterns, **HOF pattern (`_Class`/`Class`)** and **Decorator pattern**:
244
+
245
+ ### 1. HOF pattern (`_Class`/`Class`)
246
+
247
+ Define implementation in an untyped class, then wrap for typing:
248
+
249
+ ```ts
250
+ import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
251
+
252
+ class _X extends Sigil {
253
+ // Class logic here
254
+ }
255
+ export const X = withSigilTyped(_X, 'Label.X');
256
+ export type X = GetInstance<typeof X>;
257
+ ```
258
+
259
+ #### `InstanceType<>` vs `GetInstance<>`
260
+
261
+ You should depend on `GetInstance` to get type of instance and avoid using `InstanceType` as it returns `any` if the class constructor is `protected` or `private`.
262
+
263
+ ```ts
264
+ export type X = GetInstance<typeof X>; // <-- works with 'private' and 'protected' constructors as well
265
+ ```
266
+
267
+ Internally `GetInstance` is just `T extends { prototype: infer R }`.
268
+
269
+ #### Generic propagation
270
+
271
+ ```ts
272
+ class _X<G> extends Sigil {}
273
+ export const X = withSigilTyped(_X, 'Label.X');
274
+ export type X<G> = GetInstance<typeof X<G>>; // <-- Redeclare generics here
275
+
276
+ class _Y<G> extends X<G> {} // and so on...
277
+ ```
278
+
279
+ #### Anonymous classes
280
+
281
+ You may see error: `Property 'x' of exported anonymous class type may not be private or protected.`, although this is rare to occur.
282
+ This comes from the fact that all typed classes are `anonymous class` as they are return of HOF. to avoid these error entirely all you need is exporting the untyped classes even if they are un-used as a good convention.
283
+
284
+ ```ts
285
+ export class _X extends Sigil {} // <-- Just add 'export' here
286
+ export const X = withSigilTyped(_X, 'Label.X');
287
+ export type X = GetInstance<typeof X>;
288
+ ```
289
+
290
+ #### Private constructors
291
+
292
+ The only limitation in HOF approach is **extending private constructors**:
293
+
294
+ ```ts
295
+ import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
296
+ class _X extends Sigil {
297
+ private constructor() {}
298
+ }
299
+ const X = withSigilTyped(_X, 'X');
300
+ type X = GetInstance<typeof X>;
301
+
302
+ class _Y extends X {} // <-- This is allowed!
303
+ const Y = withSigilTyped(_Y, 'Y');
304
+ type Y = GetInstance<typeof Y>;
305
+
306
+ const y = new Y(); // <-- Type here is any
307
+ ```
308
+
309
+ Unfortunately this is a limitation in typescript and I couldn't find any solution to address it.
310
+
311
+ ---
312
+
313
+ ### 2. Decorator pattern
314
+
315
+ Inject brand directly in class body:
316
+
317
+ ```ts
318
+ import { Sigil, WithSigil, UpdateSigilBrand } from '@vicin/sigil';
319
+
320
+ @WithSigil('X')
321
+ class X extends Sigil {
322
+ declare __SIGIL_BRAND__: UpdateSigilBrand<'X', Sigil>;
323
+ }
324
+
325
+ @WithSigil('Y')
326
+ class Y extends X {
327
+ declare __SIGIL_BRAND__: UpdateSigilBrand<'Y', X>;
328
+ }
329
+ ```
330
+
331
+ No `_Class`/`Class` pattern, no `private constructor` issue, no type hacks and only one extra line, but our branding logic now lives in class body.
332
+
333
+ #### Label Consistency
334
+
335
+ Use typeof label for compile-time matching:
336
+
337
+ ```ts
338
+ import { Sigil, WithSigil, UpdateSigilBrand } from '@vicin/sigil';
339
+
340
+ const label = 'X';
341
+
342
+ @WithSigil(label)
343
+ class X extends Sigil {
344
+ declare __SIGIL_BRAND__: UpdateSigilBrand<typeof label, Sigil>;
345
+ }
346
+ ```
347
+
348
+ ---
349
+
350
+ ## API reference
351
+
352
+ ### Primary Exports
353
+
354
+ - **Mixin:**
355
+ - `Sigilify(Base, label?, opts?)`
356
+
357
+ - **Classes:**
358
+ - `Sigil`
359
+ - `SigilError`
360
+
361
+ - **Decorator:**
362
+ - `WithSigil(label, opts?)`
363
+
364
+ - **HOFs:**
365
+ - `withSigil(Class, label?, opts?)`
366
+ - `withSigilTyped(Class, label?, opts?)`
367
+
368
+ - **Helpers:**
369
+ - `isSigilCtor(ctor)`
370
+ - `isSigilInstance(inst)`
371
+ - `isSigilBaseCtor(ctor)`
372
+ - `isSigilBaseInstance(inst)`
373
+ - `isDecorated(ctor)`
374
+ - `isInheritanceChecked(ctor)`
375
+
376
+ - **Options/Registry:**
377
+ - `updateOptions(opts, mergeRegistries?)`
378
+ - `SigilRegistry`
379
+ - `getActiveRegistry`
380
+ - `DEFAULT_LABEL_REGEX`
381
+ - **Types:**
382
+ - `ISigil<Label, ParentSigil?>`
383
+ - `ISigilStatic<Label, ParentSigil?>`
384
+ - `ISigilInstance<Label, ParentSigil?>`
385
+ - `SigilBrandOf<T>`
386
+ - `TypedSigil<SigilClass, Label>`
387
+ - `GetInstance<T>`
388
+ - `UpdateSigilBrand<Label, Base>`
389
+ - `SigilOptions`
390
+
391
+ ### Key helpers (runtime)
392
+
393
+ - `Sigil`: a minimal sigilified base class you can extend from.
394
+ - `SigilError`: an `Error` class decorated with a `Sigil` so it can be identified at runtime.
395
+ - `WithSigil(label)`: class decorator that attaches `Sigil` metadata at declaration time.
396
+ - `Sigilify(Base, label?, opts?)`: mixin function that returns a new constructor with `Sigil` types and instance helpers.
397
+ - `withSigil(Class, label?, opts?)`: HOF that validates and decorates an existing class constructor.
398
+ - `withSigilTyped(Class, label?, opts?)`: like `withSigil` but narrows the TypeScript type to include brands.
399
+ - `isSigilCtor(value)`: `true` if `value` is a `Sigil` constructor.
400
+ - `isSigilInstance(value)`: `true` if `value` is an instance of a `Sigil` constructor.
401
+ - `SigilRegistry`: `Sigil` Registry class used to centralize classes across app.
402
+ - `getActiveRegistry`: Getter of active registry being used by `Sigil`.
403
+ - `updateOptions(opts, mergeRegistries?)`: change global runtime options before `Sigil` decoration (e.g., `autofillLabels`, `devMarker`, etc.).
404
+ - `DEFAULT_LABEL_REGEX`: regex that ensures structure of `@scope/package.ClassName` to all labels, it's advised to use it as your `SigilOptions.labelValidation`
405
+
406
+ ### Instance & static helpers provided by Sigilified constructors
407
+
408
+ When a constructor is decorated/sigilified it will expose the following **static** getters/methods:
409
+
410
+ - `SigilLabel` the human label string.
411
+ - `SigilType` — the runtime symbol for the label.
412
+ - `SigilTypeLineage` — readonly array of symbols representing parent → child.
413
+ - `SigilTypeSet` readonly `Set<symbol>` for O(1) checks.
414
+ - `isSigilified(obj)` — runtime predicate that delegates to `isSigilInstance`.
415
+ - `isOfType(other)` — O(1) membership test using `other`'s `__TYPE_SET__`.
416
+ - `isOfTypeStrict(other)` — strict lineage comparison element-by-element.
417
+
418
+ Instances of sigilified classes expose instance helpers:
419
+
420
+ - `getSigilLabel()` — returns the human label.
421
+ - `getSigilType()` — runtime symbol.
422
+ - `getSigilTypeLineage()` returns lineage array.
423
+ - `getSigilTypeSet()` — returns readonly Set.
424
+
425
+ ---
426
+
427
+ ## Options & configuration
428
+
429
+ Customize behavior globally at startup:
430
+
431
+ ```ts
432
+ import { updateOptions, SigilRegistry } from '@vicin/sigil';
433
+
434
+ updateOptions({
435
+ autofillLabels: false, // Automatically label unlabeled subclasses
436
+ skipLabelInheritanceCheck: false, // Bypass dev inheritance checks -- ALMOST NEVER WANT TO SET THIS TO TRUE, Use 'autofillLabels: true' instead.
437
+ labelValidation: null, // Function or regex, Enforce label format
438
+ devMarker: process.env.NODE_ENV !== 'production', // Toggle dev safeguards
439
+ registry: new SigilRegistry(), // Custom registry instance
440
+ useGlobalRegistry: true, // Store in 'globalThis' for cross-bundle access
441
+ storeConstructor: true, // Include constructors in registry
442
+ });
443
+ ```
444
+
445
+ Values defined in previous example are defaults, per-class overrides available in mixin, decorators, and HOFs.
446
+
447
+ ---
448
+
449
+ ## Registry
450
+
451
+ The registry ensures **unique labels** and supports central class management for ops as serialization.
452
+
453
+ - **Access:** `const registry = getActiveRegistry();` – Returns current `SigilRegistry` or `null`.
454
+ - **Operations:** `has(label)`, `get(label)`, `listLabels()`, `register(label, ctor, opts?)`, `unregister(label)`, `clear()`, `size`.
455
+ - **Replacement:** `updateOptions({ registry: new SigilRegistry(myMap) }, merge?);` – Optionally merge existing entries.
456
+ - **Disable:** Set `registry: null` to skip all registry functions.
457
+ - **Global Storage:** Defaults to `globalThis[Symbol.for('__SIGIL_REGISTRY__')];` disable with `useGlobalRegistry: false` if single-bundle guaranteed.
458
+ - **Constructor Privacy:** Set `storeConstructor: false` globally or per-class to replace constructors with null in the map.
459
+
460
+ ### Class typing in registry
461
+
462
+ Unfortunately concrete types of classes is not supported and all classes are stored as `ISigil` type. if you want concrete typing, you can wrap registry:
463
+
464
+ ```ts
465
+ import { getActiveRegistry } from '@vicin/sigil';
466
+ import type { MySigilClass1 } from './file1';
467
+ import type { MySigilClass2 } from './file2';
468
+
469
+ interface MyClasses {
470
+ MySigilClass1: typeof MySigilClass1;
471
+ MySigilClass2: typeof MySigilClass2;
472
+ }
473
+
474
+ export class MySigilRegistry {
475
+ listLabels(): (keyof MyClasses)[] {
476
+ return getActiveRegistry()?.listLabels();
477
+ }
478
+ has(label: string): boolean {
479
+ return getActiveRegistry()?.has(label);
480
+ }
481
+ get<L extends keyof MyClasses>(label: L): MyClasses[L] {
482
+ return getActiveRegistry()?.get(label) as any;
483
+ }
484
+ unregister(label: string): boolean {
485
+ return getActiveRegistry()?.unregister(label);
486
+ }
487
+ clear(): void {
488
+ getActiveRegistry()?.clear();
489
+ }
490
+ replaceRegistry(newRegistry: Map<string, ISigil> | null): void {
491
+ getActiveRegistry()?.replaceRegistry(newRegistry);
492
+ }
493
+ get size(): number {
494
+ return getActiveRegistry()?.size;
495
+ }
496
+ }
497
+ ```
498
+
499
+ Now you have fully typed central class registry!
500
+
501
+ ---
502
+
503
+ ## Security guidance
504
+
505
+ - **Recommended for Untrusted Environments:** `updateOptions({ storeConstructor: false });` – Prevents constructors from being stored in the registry map (labels remain, but constructors are `null`).
506
+
507
+ - **Trusted Environments:** Enable full registry for centralization (default behavior).
508
+
509
+ - **Per-Class Control:** Use `{ storeConstructor: false }` for sensitive classes in decorator or HOF function.
510
+
511
+ Always remember, Registry is metadata-only; avoid for sensitive data. Global access possible if enabled.
512
+
513
+ ---
514
+
515
+ ## Minimal mode
516
+
517
+ `updateOptions({ autofillLabels: true, storeConstructor: false });` – Enables background operation without explicit labels or storage:
518
+
519
+ ```ts
520
+ import { Sigil, updateOptions } from '@vicin/sigil';
521
+
522
+ // run at the start of the app
523
+ updateOptions({ autofillLabels: true, storeConstructor: false });
524
+
525
+ // No decorators or HOF needed to use 'isOfType' ('instanceof' replacement)
526
+ class A extends Sigil {}
527
+ class B extends A {}
528
+ class C extends B {}
529
+ ```
530
+
531
+ ---
532
+
533
+ ## Troubleshooting & FAQ
534
+
535
+ - **Dev Extension Errors:** Add labels or enable autofillLabels.
536
+ - **Anonymous Class Errors:** Export untyped bases.
537
+ - **Selective Labeling:** Use `autofillLabels: true` or empty `@WithSigil()` for auto-generation.
538
+ - **Registry Inspection:** `getActiveRegistry()?.listLabels()`.
539
+
540
+ ---
541
+
542
+ ## Deprecated API
543
+
544
+ ### REGISTRY
545
+
546
+ `Sigil` has moved from static reference registry to dynamic access and updates, now devs can create `SigilRegistry` class and pass it to `SigilOptions` to be used by the library internals. however change is done gracefully and `REGISTRY` is still supported with no change in behavior but it's **marked with `deprecated` and will be removed in v2.0.0**.
547
+
548
+ ```ts
549
+ import { REGISTRY, getActiveRegistry } from '@vicin/sigil';
550
+
551
+ // from:
552
+ const present = REGISTRY.has('label');
553
+ // to:
554
+ const registry = getActiveRegistry();
555
+ const present = registry ? registry.has('label') : false;
556
+ ```
557
+
558
+ ```ts
559
+ import { REGISTRY, updateOptions, SigilRegistry } from '@vicin/sigil';
560
+
561
+ // from:
562
+ const newRegistry = new Map();
563
+ REGISTRY.replaceRegistry(newRegistry);
564
+ // to
565
+ const newRegistry = new SigilRegistry(); // can pass external map to constructor, this map will hold all classes
566
+ updateOptions({ registry: newRegistry });
567
+ ```
568
+
569
+ ### typed
570
+
571
+ Obsolete; mixins now handle typing natively. **marked with `deprecated` and will be removed in v2.0.0**
572
+
573
+ ---
574
+
575
+ ## Phantom
576
+
577
+ `Phantom` is another lightweight TypeScript library I created for achieving **nominal typing** on primitives and objects through type-only metadata. It solves the problem of structural typing in TypeScript allowing accidental misuse of identical shapes (e.g., confusing `UserId` and `PostId` as both strings) by enabling compile-time distinctions with features like **brands**, **constrained identities**, **variants for states**, **additive traits**, and **reversible transformations**. This makes it ideal for domain-driven design (DDD) without runtime overhead.
578
+
579
+ `Phantom` works seamlessly in conjunction with `Sigil`, use `Sigil` for nominal identity on classes (runtime-safe checks across bundles), and `Phantom` for primitives/objects. Together, they provide **end-to-end type safety**: e.g., a Sigil-branded `User` class could hold a Phantom-branded `UserId` string property, enforcing domain boundaries at both compile and runtime.
580
+
581
+ - **GitHub: [@phantom](https://github.com/ZiadTaha62/phantom)**
582
+ - **NPM: [@phantom](https://www.npmjs.com/package/@vicin/phantom)**
583
+
584
+ ---
585
+
586
+ ## Contributing
587
+
588
+ Any contributions you make are **greatly appreciated**.
589
+
590
+ Please see our [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
591
+
592
+ ### Reporting bugs
593
+
594
+ If you encounter a bug:
595
+
596
+ - 1. Check existing issues first
597
+ - 2. Open a new issue with:
598
+ - Minimal reproduction
599
+ - Expected vs actual behavior
600
+ - Environment (Node, TS version)
601
+
602
+ Bug reports help improve Sigil — thank you! 🙏
603
+
604
+ ## License
605
+
606
+ Distributed under the MIT License. See `LICENSE` for more information.
607
+
608
+ ---
609
+
610
+ ## Author
611
+
612
+ Built with ❤️ by **Ziad Taha**.
613
+
614
+ - **GitHub: [@ZiadTaha62](https://github.com/ZiadTaha62)**
615
+ - **NPM: [@ziadtaha62](https://www.npmjs.com/~ziadtaha62)**
616
+ - **Vicin: [@vicin](https://www.npmjs.com/org/vicin)**