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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@jsoldi/hkt

v1.9.12

Published

Higher kinded types for typescript and a few utility monads.

Downloads

56

Readme

@jsoldi/hkt

Lightweight library that brings Haskell-style higher-kinded types to TypeScript. It includes common abstractions like Monad, Functor, and Monoid, along with implementations for common JavaScript objects. The idea is to provide a solid foundation for functional programming in TypeScript without abandoning JavaScript's native features.

As an example, instead of directly defining a foldable interface, the library provides a more general fold interface (with foldable as a special case), where results are wrapped in a monad. This approach allows types like AsyncGenerator to be folded by returning a task (a promise returning function), adding foldable capabilities to async generators.

The aim is for the library to integrate seamlessly into existing projects without the need to adopt a new paradigm or to rewrite existing code.

Installation

npm install @jsoldi/hkt

Examples

Piping computations

import { array } from '@jsoldi/hkt';

// Monad's `pipe` emulates Haskell's do notation. Each function 
// receives all the previous results in reverse order.
const res = array.pipe(
    ['♦️', '♣️', '♥️', '♠️'], // suits
    _ => array.range(1, 13), // ranks
    (rank, suit) => [`${rank}${suit}`] // rank-suit pairs
); 

console.log(res); // ['1♦️', '2♦️', '3♦️', '4♦️', …, '12♠️', '13♠️']

Binary counting

import { array, string } from '@jsoldi/hkt';

// This semiring uses array's `bind` to distribute addition (array 
// concatenation) over multiplication (string concatenation), 
const s = array.semiring(string);
const arg = array.replicate(5, ['0', '1']); // ['0', '1'] × 5
const res = s.times(...arg); // ('0' + '1') × ('0' + '1') … 
console.log(res); // ['00000', '00001', '00010',  …, '11111']

Higher-kinded types

Higher kinded types can be defined by extending the KRoot interface:

import { KRoot } from '@jsoldi/hkt';

// Non higher-kinded Log type
type Log<T> = [string[], T];

// Higher-kinded Log type
interface KLog extends KRoot {
    readonly 0: unknown;
    readonly body: Log<this[0]>;
}

To apply arguments to higher-kinded types, use the $ operator like $<F, A>, where F is a higher-kinded type and A is the type argument:

import { $ } from '@jsoldi/hkt';

// …
type NumberLog = $<KLog, number>; // Evaluates to `Log<number>`

Higher kinded types can themselves be used as type arguments. For instance, this is the IMonadBase definition, which can be passed to the monad function to produce an IMonad:

export interface IMonadBase<F> {
    unit<A>(a: A): $<F, A>
    bind<A, B>(fa: $<F, A>, f: (a: A) => $<F, B>): $<F, B>
}

Defining a custom monad

import { KRoot, monad } from '@jsoldi/hkt';

// Non higher-kinded Log type
type Log<T> = [string[], T];

// Higher-kinded Log type
interface KLog extends KRoot {
    readonly 0: unknown;
    readonly body: Log<this[0]>;
}

const logger = {
    // Custom monad implementation
    ...monad<KLog>({
        unit: a => [[], a], 
        bind: ([logA, a], f) => {
            const [logB, b] = f(a); 
            return [[...logA, ...logB], b]; // Concatenate logs
        },
    }),
    log: <A>(log: string, a: A): Log<A> => [[log], a] // Add a log 
}

const add = (a: number, b: number): Log<number> => 
    logger.log(`Adding ${a} and ${b}`, a + b);

const mul = (a: number, b: number): Log<number> => 
    logger.log(`Multiplying ${a} and ${b}`, a * b);

const res = logger.pipe(
    logger.unit(1),
    x => mul(x, 2),
    x => add(x, 3)
);

console.log(res); // [["Multiplying 1 and 2", "Adding 2 and 3"], 5]

Advanced examples

Continuations and Trampolines

import { pipe, TypeArg, cont, ContVoid } from '@jsoldi/hkt';

// Stack-safe trampoline which combines continuations and thunks
const t = cont.trampoline;
type Trampoline<T> = TypeArg<typeof t, T>;
// Trampoline<T> = <R>(resolve: (t: T) => Free<R, KLazy>) => Free<R, KLazy>

// Fibonacci sequence using trampoline and memoization
const fibonacci = t.memo((n: bigint): Trampoline<bigint> => {
    if (n < 2)
        return t.unit(n);

    return t.pipe(
        t.suspend(() => fibonacci(n - 1n)),
        _ => t.suspend(() => fibonacci(n - 2n)),
        (m1, m2) => t.unit(m1 + m2)
    );
});

// Prompt function as a void returning continuation
const prompt = (message: string): ContVoid<string> => resolve => {
    console.log(message);

    process.stdin.addListener("data", function listen(data) {
        process.stdin.removeListener("data", listen);
        resolve(data.toString().trim());
    });
};

// Standalone `pipe` is the trivial (identity) monad's `pipe` — passing 
// the first argument through the (reversed) function composition of the 
// rest.
pipe(
    cont.void.map(
        prompt('Enter position in Fibonacci sequence or "exit": '),
        input => {
            try {
                if (input === 'exit') 
                    return true;

                const result = pipe(input, BigInt, fibonacci, t.run)();
                console.log('Result', result, '\n');
            } catch (e) {
                console.log('Invalid number\n');
            }
        }
    ),
    cont.void.doWhile(exit => !exit) // Loop until exit is true
)(_ => process.exit(0));

Stack-safe arithmetic parser

import { 
    cont, 
    maybe, 
    state, 
    TypeArg, 
    KRoot, 
    monadPlus, 
    chain, 
    lazy 
} from '@jsoldi/hkt';

// Alias for trampline
const t = cont.trampoline;

// Put maybe inside trampoline
const m = maybe.transform(t);

// Put trampoline inside state monad
const s = state.of<string>().transform(m); 

// Non-higher-kinded parser type
type Parser<T> = TypeArg<typeof s, T>; 
// Parser<T> = (a: string) => Cont<Maybe<[T, string]>, KFree<KLazy>>

// Higher-kinded parser type
interface KParser extends KRoot {
    readonly 0: unknown
    readonly body: Parser<this[0]>
}

const parser = (() => {
    // Create a monadPlus (monad + monoid) instance for the parser
    const base = monadPlus<KParser>({ 
        unit: s.unit,
        // Suspend the computation of the second parser for lazy evaluation
        bind: (p, f) => 
            s.bind(p, a => input => t.suspend(() => f(a)(input))),
        empty: () => _ => m.empty(),
        // `append` outputs the first non-empty result
        // No need to run the second parser if the first one succeeds
        append: (p1, p2) => input => 
            m.append(p1(input), t.suspend(() => p2(input)))
    });

    // Next character parser
    const next: Parser<string> = input => input.length === 0 
        ? m.empty() 
        : m.unit([input[0], input.slice(1)]);

    // Regex parser
    const regex = (re: RegExp): Parser<string> => (input: string) => {
        const match = input.match(re);

        return match === null
            ? m.empty()
            : m.unit([match[0], input.slice(match[0].length)]);
    }

    // Chain left-associative parser
    const chainl1 = <A>(
        p: Parser<A>, 
        op: Parser<(a: A, b: A) => A>
    ): Parser<A> => 
        base.bind(
            p,
            a => base.map(
                base.many(
                    base.bind(
                        op,
                        f => base.map(p, b => (x: A) => f(x, b))
                    )
                ),
                vs => vs.reduce((a, f) => f(a), a)
            )
        );

    // Character parser
    const char = (c: string) => base.filter<string>(s => s === c)(next);

    return { ...base, next, regex, chainl1, char }
})();

const math = (() => {
    // Number parser
    const num = parser.map(parser.regex(/^\d+(\.\d+)?/), parseFloat);

    // Addition and subtraction parser
    const addOp = parser.append(
        parser.map(parser.char('+'), _ => (a: number, b: number) => a + b),
        parser.map(parser.char('-'), _ => (a: number, b: number) => a - b)
    );

    // Multiplication and division parser
    const mulOp = parser.append(
        parser.map(parser.char('*'), _ => (a: number, b: number) => a * b),
        parser.map(parser.char('/'), _ => (a: number, b: number) => a / b)
    );

    // Numbers and groups parser
    const group: Parser<number> = parser.append(
        num,
        parser.pipe(
            parser.char('('),
            _ => expr,
            _ => parser.char(')'),
            (_, n) => parser.unit(n)
        )
    );

    // Arithmetic expression parser
    const expr = parser.chainl1(
        parser.chainl1(
            group,  // Parse numbers and groups first
            mulOp   // Then parse multiplication and division
        ),
        addOp       // Finally parse addition and subtraction
    );

    // Remove whitespace and parse expression
    const parse = chain(
        (s: string) => s.replace(/\s/g, ''), 
        expr, 
        m.fmap(([n]) => n),
        m.else(() => t.unit('Invalid expression')),
        t.run,
        lazy.run
    );

    return { ...parser, num, addOp, mulOp, group, expr, parse }
})();

console.log(math.parse('10.1 + 20 * 30 + 40')); // 650.1

// Trampoline is stack-safe
console.log(math.parse(
    Array.from({ length: 10000 }, (_, i) => i).join('+')
)); // 49995000