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 🙏

© 2024 – Pkg Stats / Ryan Hefner

runtime-branding

v0.2.0

Published

Runtime Branding API for TypeScript

Downloads

8

Readme

runtime-branding

A compile time & runtime object metadata branding API for TypeScript, basically it's like nominal types with branding but with runtime support. This library allows the creation of custom branding functions for creating nominal types (compile time) and linking metadata to objects lifecycle (runtime), without changing the objects structure or content (yes, we are immutable-friendly).

Requirements & Installation

This library uses WeakSet, the minimum runtime requirement is ES6.

Installation:

npm i --save branding-metadata

Example

The ideas behind this library are rooted in the concept of type branding, that is a technique used to refine types for a better (and safer) development experience, leveraging the intersection types. Furthermore, there are some scenarios in which would be useful to link contextual information at runtime. The following example shows how to keep track of the origin of a request, that can be an external integration service or an internal soruce.

Step 1: define the branding functions.

// source-branding.ts
interface SourceBrand {
    source: 'external';
}
const brand: SourceBrand = { source: 'external' };
export const externalSourceBranding = createBranding<SourceBrand>(brand);

Step 2: brand object as they get into the system

// external-endpoint.ts
import { externalSourceBranding, SourceBrand } from './source-branding.ts'
import { invokeBusinessLogic } from './core.ts'
import { Branded } from 'runtime-branding'
export function onExternalRequest(req: Request, res: Response) {
    const brandedReq: Request & Branded<SourceBrand> = externalSourceBranding(req);
    invokeBusinessLogic(brandedReq, res);
}

Step 3: read object branding metadata

// core.ts
import { externalSourceBranding, SourceBrand } from './source-branding.ts'
import { Branded } from 'runtime-branding'
export function invokeBusinessLogic(req: Request, res: Response) {
    
    if (isOrderRequest(req) && req.order.total > 5000 && externalSourceBranding.has(req)) {
        // here req type is Request & Branded<SourceBrand>, because of the type guard
        res.status(400).send({error: 'This order cannot be confirmed from external services'});
        return;
    }

    // logic here...
}

There are some things to consider here:

  • The invokeBusinessLogic method may be called from many different sources, but it has some business constraints related to the source of the request (and order total value, in this case).
  • The original request has not been changed (no additional fields): the runtime branding would work with immutable objects, too.
  • There is no need to pass a "source" flag or object as an additional parameter: if invokeBusinessLogic was behind some kind of routing middleware, nothing should be changed to make this work.
  • The branding type guard allows to build conditional logic for routing objects to functions that enforce brand type check on input parameters.
  • The same object may be branded by many branding functions

Last but not least, we may add a branding callback function to the externalSourceBranding in order to execute some custom code when an object is branded:

export const externalSourceBranding = createBranding<SourceBrand>(brand, (obj, brand) => {
    // custom metadata handling here.
});

There are many different scenarios for runtime branding (see examples):

  • Get "current" user/session from a request object.
  • Request/response (or command/events) correlation, without adding additional data.
  • Transactional boundary management (retrieve the "current" transaction from request object)
  • [Type|priority|version|etc.]-based message routing.
  • Type metadata for serialization/deserialization.

Documentation

The main entry point is the createBranding function:

declare function createBranding<B extends object, X extends object = object>
        (brandObject: B, callback?: BrandingCallback<B, X>): Branding<B, X>;

The return type Branding<B, X> is the branding function. Since objects can be branded by different branding functions, the brandObject param should have a unique shape, in order to avoid type clash between brands. Symbols may be useful to achieve brand uniqueness:

const symA = Symbol()
type BrandA = { [symA]: object;}

const symB = Symbol()
type BrandB = { [symB]: object }

const brandingA = createBranding<BrandA>({ [symA]: {} });
const brandingB = createBranding<BrandB>({ [symB]: {} });

interface Person {
    id: number;
    name: string;
}

const person1: Person = { id: 123, name: 'foo' };
const person2 = brandingA(person1);  // type: Person & Brand<BrandA>
const person3 = brandingB(person2);  // type: Person & Brand<BrandA> & Brand<BrandB>

The second parameter of createBranding is a callback function invoked for each branded object:

const brandingA = createBranding<BrandA>({ [symA]: {} }, (obj, brand) => {
    console.log('Branding an object!');
});
const person2 = brandingA(person1); // prints 'Branding an object!'

Type guard & assertion

The has() and assert() static methods implement runtime type guard and assertion:

const person : Person = readPerson(123);
if (brandingA.has(person)) {
    // here person const has type: Person & Brand<BrandA>
}

/*
the following is needed due to a Typescript design limitation.
See https://github.com/microsoft/TypeScript/pull/32695
*/
const assertBrandA : typeof brandingA['assert'] = brandingA.assert; 

assertBrandA(person) // throws if person object is not branded with BrandA

// here person const has type: Person & Brand<BrandA>

Branding merge & refine

The merge() static method allows to merge two branding functions.

const mergedBranding = brandingA.merge(brandingB);
const person1 = mergedBranding(person) // type: Person & Brand<BrandA & BrandB>

The merge() method is commutative:

const mergedBranding = brandingB.merge(brandingA); // it's equivalent to brandingA.merge(brandingB)
const person1 = mergedBranding(person) // type: Person & Brand<BrandA & BrandB>

If merged brandings were created with callbacks, both of them would be called:

const brandingA = createBranding<BrandA>({ [symA]: {} }, (obj, brand) => console.log('Branding A'));
const brandingB = createBranding<BrandB>({ [symB]: {} }, (obj, brand) => console.log('Branding B'));
const mergedBranding = brandingA.merge(brandingB);
const person1 = mergedBranding(person); // prints 'Branding A' and 'Branding B'

The refine() method allows to specialize an existing branding with a new brand object:

const brandingA = createBranding<BrandA>({ [symA]: {} });
const brandingB = brandingA.refine<BrandB>({ [symB]: {} });
const person1 = mergedBranding(person); //  Person & Brand<BrandB & BrandA>

The refine() method is equivalent to a createBranding() and an implicit merge(). It's basically a useful shortcut for creating many sub-brands from a base one.