npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vicin/sigil

v3.4.0

Published

Sigil gives you the power of safe cross-bundle class instances checks and simple class nominal typing if needed

Readme

Sigil

npm version npm downloads License: MIT TypeScript Build bundle size

  • 🎉 v3.0.0 is out! Happy coding! 😄💻
  • 📄 Changelog: CHANGELOG.md

Sigil gives you the power of safe cross-bundle class instances checks and simple class nominal typing if needed.

Features

  • Drop-in instanceof replacement that works across bundles, HMR and monorepos, and adds an exact-class-instance check
  • Simple nominal typing with just one line of code for each class (e.g., UserId vs. PostId)
  • Tiny less than 1.6 KB minified and brotlied measured using size-limit
  • Performant as native instanceof but with guaranteed checks
  • Test coverage is 100% to ensure that runtime remains consistent and predictable
  • Safe with strict rules to ensure uniqueness of labels, if duplicate label is passed error is thrown immediately

Table of contents


Quick start

Install

npm install @vicin/sigil
# or
yarn add @vicin/sigil
# or
pnpm add @vicin/sigil

Requires TypeScript 5.0+ for decorators; attach functions work on older versions. Node.js 18+ recommended.

No tsconfig changes are needed as we use Stage 3 decorators which are supported by default in TypeScript 5.0+

Basic usage

Opt into Sigil

Use the Sigil base class or the Sigilify mixin to opt a class into the Sigil runtime contract.

import { Sigil, Sigilify } from '@vicin/sigil';

// Using the pre-sigilified base class:
class User extends Sigil {}

// Or use Sigilify when you want an ad-hoc class:
const MyClass = Sigilify(class {}, '@myorg/mypkg.MyClass');

If your class is marked with abstract:

import { Sigil, SigilifyAbstract } from '@vicin/sigil';

abstract class User extends Sigil {}

const MyClass = SigilifyAbstract(abstract class {}, '@myorg/mypkg.MyClass');

Extend Sigil classes

After opting into the Sigil contract, labels are passed to child classes to uniquely identify them, they can be supplied using two patterns:

Decorator pattern

Apply a label with the @AttachSigil decorator:

import { Sigil, AttachSigil } from '@vicin/sigil';

@AttachSigil('@myorg/mypkg.User')
class User extends Sigil {}
Function pattern

Apply a label using attachSigil function:

import { Sigil, attachSigil } from '@vicin/sigil';

class User extends Sigil {}
attachSigil(User, '@myorg/mypkg.User');

Note: Function pattern is susceptible to some Edge case subtle pitfalls if not used appropriately, so we advise to use decorator pattern

Migration

Migrating old code into Sigil can be done with extra couple lines of code only:

  1. Pass your base class to Sigilify mixin:
import { Sigilify } from '@vicin/sigil';

const MySigilBaseClass = Sigilify(MyBaseClass);
  1. Or extend it with Sigil:
import { Sigil } from '@vicin/sigil';

class MyBaseClass extends Sigil {} // <-- add 'extends Sigil' here

Congratulations — you’ve opted into Sigil and you can start replacing instanceof with isOfType() / isExactType(), however there is more to add to your system, check Core concepts for more.


Core concepts

Terminology

  • Label: An identity (string) such as @scope/pkg.ClassName, must be unique for each Sigil class otherwise error is thrown.
  • EffectiveLabel: A human-readable (string) such as @scope/pkg.ClassName, if no label is passed it inherit the last defined label.
  • isOfType: Takes object argument and check if this object is an instance of calling class or it's children. Can be called from class instances as well.
  • isExactType: Takes object argument and check if this object is an instance of calling class only. Can be called from class instances as well.
  • [sigil]: TypeScript symbol marker for nominal types.

Purpose and Origins

Sigil addresses issues in large monorepos, HMR:

  • Unreliable instanceof: Bundling cause class redefinitions, breaking checks.
// Can be broken in monorepo or HMR set-ups
if (obj instanceof User) { ... }

// With Sigil
if (User.isOfType(obj)) { ... } // This still works even if User was bundled twice.
if (User.isExactType(obj)) { ... } // Or check for exactly same constructor not its children

Also by utilizing unique passed labels it solves another problem in Domain-Driven Design (DDD):

  • Manual Branding Overhead: Custom identifiers lead to boilerplate and maintenance issues, Sigil adds reliable inheritance-aware nominal branding with just one line of code.
import { sigil } from '@vicin/sigil';

class User extends Sigil {
  declare [sigil]: ExtendSigil<'User', Sigil>; // <-- Update nominal brand with this line
}

type test1 = User extends Sigil ? true : false; // true
type test2 = Sigil extends User ? true : false; // false

Implementation Mechanics

  • Runtime Contract: Established via extending Sigil or using Sigilify mixin.
  • Update metadata: With each new child, use decorator (AttachSigil) or function (attachSigil) to attach run-time metadata, also use ExtendSigil on [sigil] field to update nominal type.
import { Sigil, AttachSigil, sigil, ExtendSigil } from '@vicin/sigil';

@AttachSigil('@scope/package.MyClass') // <-- Run-time values update
class MyClass extends Sigil {
  declare [sigil]: ExtendSigil<'MyClass', Sigil>; // <-- compile-time type update
}

You can avoid decorators and use normal functions if needed:

import { Sigil, attachSigil, sigil, ExtendSigil } from '@vicin/sigil';

class MyClass extends Sigil {
  declare [sigil]: ExtendSigil<'MyClass', Sigil>;
}

attachSigil(MyClass, '@scope/package.MyClass');

Example

import { Sigil, AttachSigil } from '@vicin/sigil';

@AttachSigil('@myorg/User')
class User extends Sigil {
  declare [sigil]: ExtendSigil<'User', Sigil>;
}

@AttachSigil('@myorg/Admin')
class Admin extends User {
  declare [sigil]: ExtendSigil<'Admin', User>;
}

const admin = new Admin();
const user = new User();

// Instanceof like behavior
console.log(Admin.isOfType(admin)); // true
console.log(Admin.isOfType(user)); // false
console.log(User.isOfType(admin)); // true
console.log(User.isOfType(user)); // true

// Exact checks
console.log(Admin.isExactType(admin)); // true
console.log(Admin.isExactType(user)); // false
console.log(User.isExactType(user)); // true
console.log(User.isExactType(admin)); // false (Admin is child indeed but this checks for user specifically)

// Can use checks from instances
console.log(admin.isOfType(user)); // false
console.log(user.isOfType(admin)); // true
console.log(admin.isExactType(user)); // false
console.log(user.isExactType(admin)); // false

// Type checks are nominal
type test1 = Admin extends User ? true : false; // true
type test2 = User extends Admin ? true : false; // false

// Passed label must be unique (enforced by Sigil) so can be used as stable Id for class
// Also 'SigilLabelLineage' is useful for logging & debugging
console.log(Admin.SigilLabel); // '@myorg/Admin'
console.log(Admin.SigilEffectiveLabel); // '@myorg/Admin'
console.log(Admin.SigilLabelLineage); // ['Sigil', '@myorg/User', '@myorg/Admin']
console.log(admin.getSigilLabel()); // '@myorg/Admin'
console.log(admin.getSigilEffectiveLabel()); // '@myorg/Admin'
console.log(admin.getSigilLabelLineage()); // ['Sigil', '@myorg/User', '@myorg/Admin']

Errors & throws

Run-time errors that can be thrown by Sigil:

Double Sigilify

class A {}
Sigilify(Sigilify(A, 'A'), 'B'); // Throws: [Sigil Error] Class 'Sigilified' with label 'A' is already sigilified

@AttachSigil('B')
@AttachSigil('A')
class A extends Sigil {} // Throws: [Sigil Error] Class 'A' with label 'A' is already sigilified

class A extends Sigil {}
attachSigil(attachSigil(A, 'A'), 'B'); // Throws: [Sigil Error] Class 'A' with label 'A' is already sigilified

@AttachSigil() / attachSigil() on non-Sigil class

@AttachSigil('A') // Throws: [Sigil Error] 'AttachSigil' decorator accept only Sigil classes but used on class 'A'
class A {}

attachSigil(class A {}); // Throws: [Sigil Error] 'AttachSigil' function accept only Sigil classes but used on class 'A'

No label is passed with autofillLabels: false

updateSigilOptions({ autofillLabels: false });

class A extends Sigil {}
new A(); // Throws: [Sigil Error] Class 'A' is not sigilified, Make sure to sigilify all Sigil classes or set 'autofillLabels' to 'true'

Same label is passed twice to Sigil

@AttachSigil('Label')
class A extends Sigil {}

@AttachSigil('Label')
class B extends Sigil {} // Throws: [Sigil Error] Passed label 'Label' to class 'B' is re-used, passed labels must be unique

Invalid label format

updateSigilOptions({ labelValidation: RECOMMENDED_LABEL_REGEX });

@AttachSigil('InvalidLabel')
class A extends Sigil {} // Throws: [Sigil Error] Invalid Sigil label 'InvalidLabel'. Make sure that supplied label matches validation regex or function

Using '@Sigil-auto' prefix

@AttachSigil('@Sigil-auto:label')
class X extends Sigil {} // Throws: '@Sigil-auto' is a prefix reserved by the library

Invalid options passed to updateOptions

updateSigilOptions({ autofillLabels: {} as any }); // Throws: 'updateSigilOptions.autofillLabels' must be boolean
updateSigilOptions({ labelValidation: 123 as any }); // Throws: 'updateSigilOptions.labelValidation' must be null, function or RegExp
updateSigilOptions({ skipLabelUniquenessCheck: 'str' as any }); // Throws: 'updateSigilOptions.skipLabelUniquenessCheck' must be boolean

API reference

Primary Exports

  • Mixins:

    • Sigilify(Base, label, opts?)
    • SigilifyAbstract(Base, label, opts?)
  • Classes:

    • Sigil
    • SigilError
  • Decorator:

    • AttachSigil(label, opts?)
  • Attach function:

    • attachSigil(Class, label, opts?)
  • Helpers:

    • isSigilCtor(ctor)
    • isSigilInstance(inst)
    • getSigilLabels()
  • Options:

    • updateSigilOptions(opts)
    • RECOMMENDED_LABEL_REGEX
  • Types:

    • ISigil<Label, ParentSigil?>
    • ISigilStatic<Label, ParentSigil?>
    • ISigilInstance<Label, ParentSigil?>
    • SigilOf<T>
    • ExtendSigil<Label, Parent>
    • GetPrototype<Class>
    • SigilOptions

Key helpers (runtime)

  • Sigil: a minimal sigilified base class you can extend from.
  • SigilError: an Error class decorated with a Sigil so it can be identified at runtime.
  • Sigilify(Base, label, opts?): mixin function that returns a new constructor with Sigil types and instance helpers.
  • SigilifyAbstract(Base, label, opts?): Same as Sigilify but for abstract classes.
  • AttachSigil(label, opts?): class decorator that attaches Sigil metadata at declaration time.
  • attachSigil(Class, label, opts?): function that validates and decorates an existing class constructor.
  • isSigilCtor(value): true if value is a Sigil constructor.
  • isSigilInstance(value): true if value is an instance of a Sigil constructor.
  • getSigilLabels(): Get Sigil labels registered.
  • updateSigilOptions(opts): change global runtime options of Sigil library (e.g., autofillLabels).
  • RECOMMENDED_LABEL_REGEX: regex that ensures structure of @scope/package.ClassName to all labels, it's advised to use it as your SigilOptions.labelValidation

Instance & static helpers provided by Sigilified constructors

When a constructor is sigilified it will expose the following static getters/methods:

  • SigilLabel — the identity label string.
  • SigilEffectiveLabel — the human label string.
  • SigilLabelLineage — readonly array of labels representing parent → child for debugging.
  • isOfType(other) — check if other is an instance of this constructor or its children.
  • isExactType(other) — check if other is an instance exactly this constructor.

Instances of sigilified classes expose instance helpers:

  • getSigilLabel() — returns the identity label.
  • getSigilEffectiveLabel() — returns the human label.
  • getSigilLabelLineage() — returns lineage array.
  • isOfType(other) — check if other is an instance of the same class or its children as this.
  • isExactType(other) — check if other is an instance exactly the same constructor.

Options & configuration

Customize behavior globally at startup:

import { updateSigilOptions } from '@vicin/sigil';

updateSigilOptions({
  autofillLabels: true, // Automatically label unlabeled subclasses
  labelValidation: null, // Function or regex, Enforce label format
  skipLabelUniquenessCheck: false, // Skip uniqueness check for labels, should be used in HMR set-ups only
});

Values defined in previous example are defaults, per-class overrides available in mixin and attach function / decorator.


Minimal mode

By default Sigil works with minimal mode, You can ignore all decorators and functions and just make base class extend Sigil:

import { Sigil, updateSigilOptions } from '@vicin/sigil';

// No decorators or functions needed to use 'isOfType' ('instanceof' replacement)
class A extends Sigil {}
class B extends A {}
class C extends B {}

Strict mode

If you want to enforce passing a label to every class defined in your codebase, you can set autofillLabels to false at the start of app:

import { updateSigilOptions } from '@vicin/sigil';

updateSigilOptions({ autofillLabels: false });

Now if you forgot to pass a label error is thrown at the moment you create class instance or use any of Sigil methods.


Hot module reload

HMR can cause class re-definitions, which will throw in default Sigil set-up as same label will be passed multiple times triggering duplicate label error. To avoid this you can set global options to skip label uniqueness check at the start of app:

import { updateSigilOptions } from '@vicin/sigil';

updateSigilOptions({ skipLabelUniquenessCheck: true });

But this can cause unexpected behavior if same label is used for two different classes as checks are disabled globally. If you need more strict mode you can pass this options to the re-loaded class only:

@AttachSigil('HmrClassLabel', { skipLabelUniquenessCheck: true })
class HmrClass extends Sigil {}

With this approach skipLabelUniquenessCheck affects only HmrClass, and if HmrClassLabel or any other label is re-used error is thrown immediately


Edge cases

Accessing Sigil metadata before running 'attachSigil' function

If you didn't make sure that attachSigil runs right after class declaration and used one of Sigil methods this will occur:

class A extends Sigil {}

console.log(A.SigilLabel); // returns auto-generated label (e.g. @Sigil-auto:A:6:a3f15bhl) or throws in strict mode

attachSigil(A, 'A');

console.log(A.SigilLabel); // A

To avoid this bug entirely you can use the return of attachSigil in your code so you are enforced to respect order:

class _A extends Sigil {}

const A = attachSigil(_A, 'A');
type A = InstanceType<typeof A>;

console.log(A.SigilLabel); // A

Note that you can't use InstanceType on private or protected classes, however you can use GetPrototype<T> in such cases.

Static blocks & IIFE static initializer

Decorators ensure that metadata is appended before static blocks or IIFE static initializers, however attachSigil function runs after them so accessing label inside them will return auto-generated label or throw:

class A extends Sigil {
  static IIFE = (() => {
    const label = A.SigilLabel; // returns auto-generated label (e.g. @Sigil-auto:A:6:a3f15bhl) or throws in strict mode
  })();

  static {
    const label = this.SigilLabel; // returns auto-generated label (e.g. @Sigil-auto:A:6:a3f15bhl) or throws in strict mode
  }
}

attachSigil(A, 'A');

This behavior can't be avoided, so make sure not to call any Sigil method inside them or move to decorators (@AttachSigil)


Benchmarks

Sigil is built for real-world performance. Below are the latest micro-benchmark results (run on Node.js v20.12.0).

Running Tests

To run benchmarks on your machine fetch source code from github then:

npm install
npm run bench

1. Runtime Type Checking

| Depth | instanceof (per op) | isOfType (ctor) | isOfType (instance) | isExactType (ctor) | isExactType (instance) | | ----- | --------------------- | ----------------- | --------------------- | -------------------- | ------------------------ | | 0 | 0.000010 ms | 0.000025 ms | 0.000010 ms | 0.000027 ms | 0.000012 ms | | 3 | 0.000032 ms | 0.000045 ms | 0.000027 ms | 0.000038 ms | 0.000025 ms | | 5 | 0.000034 ms | 0.000046 ms | 0.000028 ms | 0.000037 ms | 0.000026 ms | | 10 | 0.000044 ms | 0.000045 ms | 0.000029 ms | 0.000038 ms | 0.000027 ms | | 15 | 0.000058 ms | 0.000063 ms | 0.000051 ms | 0.000069 ms | 0.000053 ms |

Key takeaway:
isOfType & isExactType has practically the same performance as native instanceof, slightly slower on static calls and slightly faster on the instance side.

2. Class Definition & Instance Creation

| Scenario | Definition (per class) | Instantiation (per instance) | | ------------------------------- | ---------------------- | ---------------------------- | | Empty plain class | 0.0122 ms | 0.00019 ms | | Empty Sigil class | 0.0672 ms | 0.00059 ms | | Small (5 props + 3 methods) | 0.0172 ms | 0.00327 ms | | Large (15 props + 10 methods) | 0.0212 ms | 0.00922 ms | | Large Sigil | 0.0780 ms | 0.01177 ms | | Extended chain depth 5 – plain | 0.0897 ms | 0.01809 ms | | Extended chain depth 5 – Sigil | 0.3978 ms | 0.02020 ms | | Extended chain depth 10 – plain | 0.2042 ms | 0.05759 ms | | Extended chain depth 10 – Sigil | 0.8127 ms | 0.06675 ms |

Key takeaways:

  • Class definition is a one-time cost at module load time. Even at depth 10 the cost stays well under 1 ms per class.
  • Instance creation adds a small fixed overhead of ~0.4–0.6 µs per object, which becomes completely negligible as your classes grow in size and complexity.

Bundle Size

Less than 1.6 KB (1.52 KB) (minified + Brotli, including all dependencies)

This makes Sigil one of the smallest full-featured solutions for nominal typing + reliable runtime identity.

Running Tests

To verify bundle size fetch source code from github then:

npm install
npm run size

Tests

Reliability is a core pillar of Sigil. The library is backed by a comprehensive suite of unit tests ( 71 total tests ) that cover everything from basic mixins to edge cases.

Coverage Status

We maintain 100% test coverage across the entire codebase to ensure that runtime metadata remains consistent and predictable.

| Metric | Score | | ------ | ----- | | Stmts | 100% | | Branch | 100% | | Funcs | 100% | | Lines | 100% |

Key Test Areas

  • Mixins, Attach function & decorator: Validating Sigilify, AttachSigil and attachSigil behaviors.
  • Sigil methods: Ensuring Sigil class methods (e.g. SigilLabel, getSigilLabel) work as expected.
  • Lazy Evaluation: Ensuring metadata is attached before being accessed via Sigil methods even when no attach function or decorator is used.
  • Lineage: Verifying that isOfType and isExactType work across complex inheritance chains.
  • Error Handling: Strict validation for all errors and throws.
  • Edge cases: Known edge cases.

Running Tests

To run the test suite locally and generate a coverage report, fetch source code from github then:

npm install
npm run test:unit

Contributing

Any contributions you make are greatly appreciated.

Please see our CONTRIBUTING.md for details on our code of conduct and the process for submitting pull requests.

Reporting bugs

If you encounter a bug:

    1. Check existing issues first
    1. Open a new issue with:
    • Minimal reproduction
    • Expected vs actual behavior
    • Environment (Node, TS version)

Bug reports help improve Sigil — thank you! 🙏

License

Distributed under the MIT License. See LICENSE for more information.


Author

Built with ❤️ by Ziad Taha.