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

multimethod-type-tag-hierarchy

v0.3.0

Published

Recreation of clojure multimethods with a hierarchy based around typescript string literal templates and taged types.

Downloads

6

Readme

Multimethods that play nice with typescript

This library provides multimethods (multiple dispatch) for type hierarchies built with string literal templates https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html

Available via NPM:

npm i multimethod-type-tag-hierarchy

The hierarchy, polymorphic type tags

With release of typescript 4.3 advances in string literal templates assignability checks allow us to produce type hierarchies which play nicely with typescript typing and achieve polymorphism that does not brake from JSON serialization/deserialization cycle.


const creatureTag: `creature${string}` = `creature`;
const animalTag: `creature;animal${string}` = `creature;animal`;

let creature: creatureTag;
let animal: animalTag;

creature = animal; // this works fine
animal = creature; // this produces error

type CreatureRecord = {
    type: typeof creatureTag,
    weight: number
}

type AnimalRecord = Omit<CreatureRecord, 'type'> & {
    type: typeof animalTag,
    color: string
}

let creatureRecord: CreatureRecord;
let animalRecord: AnimalRecord;

// the following works, since animal is derived from creature 
// thus can be assigned back, 
// and `creature;animal${string}` can be assigned to `creature${string}`
creatureRecord = animalRecord;

// the following does NOT work, since creature is NOT derived fron animal, 
// thus can't be assigned to it, 
// and `creature${string}` can NOT be assigned to `creature;animal${string}`
animalRecord = creatureRecord; 

Multimethods (inspired by Clojure) build upon this and allow us to utilize functional polymorphism standalone from it's typical Object Oriented coupling with inheritance, thus in a leaner and more flexible form. This is useful with Redux and other functional libraries that don't use classes and instead separate state from methods.

Sample usage

Here is a demonstration of a tag based hierarchy cat is animal, animal is creature.

//creatureRecord.ts
import { multimethod } from 'multimethod-type-tag-hierarchy';

export const creatureTag: `creature${string}` = `creature`;

export type CreatureRecord = {
    type: tyepof creatureTag,
    weight: number
}

export const getDescription = multimethod('type', 
    creatureTag, (item: CreatureRecord, note: string) =>
        `Description: ${item.type}, ${item.weight}kg; Note: ${note}`);

// the resulting 'getDescription' method accepts any type tagged as 
// `creature${string}`, that is, types where 'type' property has a type of 
// `creature;animal${string}`, `creature;animal;cat${string}` etc.
// will be accepted by typing check no problem
//animalRecord.ts
import { CreatureRecord } from './creatureRecord';
// not strictly needed, but makes it easier to use the type,
// since all members can be imported from the same file this way
export { getDescription } from './crattureRecord'; 

export const animalTag: `creature;animal${string}` = `creature;animal`;

export type AnimalRecord = Omit<CreatureRecord, 'type'> & {
    type: typeof animalTag,
    color: string
}
//catRecord.ts
import { AnimalRecord, getDescription } from './animalRecord';
export { getDescription } from './animalRecord';

export const catTag: `creature;animal;cat${string}` = `creature;animal;cat`;

export type CatRecord = Omit<AnimalRecord, 'type'> & {
    type: typeof catTag,
    name: string
}

// override the description method for cats
getDescription.extend(catTag, (item: CatRecord, note: string) =>
            `Description: ${item.type}, ${item.weight}kg, color ${item.color}, name ${item.name}; Note: ${note}`);
//showcase.ts
import { creatureTag, CreatureRecord, getDescription } from './creatureRecord';
import { animalTag, AnimalRecord } from './animalRecord';
import { catTag, CatRecord } from './catRecord';

const creatureRecord: CreatureRecord = {
    type: creatureTag,
    weight: 4
}

const animalRecord: AnimalRecord = {
    type: animalTag,
    weight: 5,
    color: 'brown'
}

const catRecord: CatRecord = {
    type: catTag,
    weight: 6,
    color: 'black',
    name: 'Jack'
}

// notice, that we use 'getDescription' from 
// base type 'creatureRecord' for all 3 calls 

const result1 = getDescription(creatureRecord, '777');
// base method
// 'Description: creature, 4kg; Note: 777'

const result2 = getDescription(animalRecord, '888'); 
// same base method, no override
// 'Description: creature;animal, 5kg; Note: 888'

const result3 = getDescription(catRecord, '999'); 
// override for cats
// 'Description: creature;animal;cat, 6kg, color black, name Jack; Note: 999'

You also have ability to call base method from inside override. Base method resoltion moves up the tag hierarchy (in direction of less derived type). In example below, when we call method defined for a;b;c the call to this.base(...) will look for a method defined for a;b and then for a. It will take the first method encountered (though you can call this.base again from inside that method).


const aTag: `a${string}` = `a`;
const bTag: `a;b${string}` = `a;b`;
const cTag: `a;b;c${string}` = `a;b;c`;

type ARecord = { type: typeof aTag, a: number }
type BRecord = Omit<ARecord, 'type'> & { type: typeof bTag, b: number }
type CRecord = Omit<BRecord, 'type'> & { type: typeof cTag, c: number }

const cRecord = {
    type: cTag,
    a: 1,
    b: 2,
    c: 3
}

const mm = multimethod(
    'type',
    aTag, 
    (item: ARecord, note: string) =>
        `Method for ARecor; a=${item.a}; note=${note};`);

mm.override(cTag, function (a: CRecord, b) {
    const baseResult = this.base(a,b);  
    return `${baseResult} Method for CRecord; c=${a.c};`;
});

const result = mm(cRecord, "note");

assert.strictEqual(result1, 'Method for ARecor; a=1; note=note; Method for CRecord; c=3;');

Word of warning

Remember, multimethod overrides are set at runtime. Make sure all files containing overrides have been loaded before invoking the method. It may by a good idea to import all relevant files at the root of your app just to ensure this.