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

@chris5855/scats

v0.1.1-alpha.2

Published

A comprehensive TypeScript library bringing Scala's powerful functional programming paradigms to JavaScript/TypeScript, featuring immutable collections, monads, pattern matching, and more

Readme

A comprehensive TypeScript library bringing Scala's powerful functional programming paradigms to JavaScript/TypeScript, featuring immutable collections, monads, pattern matching, and more

Table of Contents

Features

  • Algebraic Data Types (ADTs) via tagged unions
  • Pattern matching inspired by Scala 3
  • Immutable collections (List, Map, Set)
  • Lazy evaluation with LazyList
  • Efficient indexed sequences with Vector
  • Option, Either, Try containers
  • For-comprehensions for monadic composition
  • Typeclasses with extension methods
  • Tuples with Tuple2 and Tuple3 implementations
  • Ordering for comparison operations
  • Resource management with Using pattern
  • Monads including State and Writer

Installation

npm

npm install @chris5855/scats

yarn

yarn add @chris5855/scats

pnpm

pnpm add @chris5855/scats

Development

Setup

# Install dependencies
npm install

# Build the project
npm run build

# Run examples
npm run example

# Run tests
npm run test

Getting Started

Option: Handling nullable values

import { Option, Some, None } from "@chris5855/scats";

// Creating options
const a = Some(42);
const b = None;
const c = Option.fromNullable(maybeNull);

// Using options
const result = a
  .map((n) => n * 2)
  .flatMap((n) => (n > 50 ? Some(n) : None))
  .getOrElse(0);

Either: Handling success/failure

import { Either, Left, Right } from "@chris5855/scats";

// Creating eithers
const success = Right(42);
const failure = Left(new Error("Something went wrong"));

// Using eithers
const result = success
  .map((n) => n * 2)
  .fold(
    (err) => `Error: ${err.message}`,
    (value) => `Success: ${value}`
  );

Try: Handling exceptions

import { Try, Success, Failure, TryAsync } from "@chris5855/scats";

// Synchronous Try
const jsonResult = Try.of(() => JSON.parse(jsonString))
  .map((data) => data.value)
  .recover((err) => "default value")
  .get();

// Asynchronous Try
const asyncResult = await TryAsync.of(async () => {
  const response = await fetch("https://api.example.com");
  return response.json();
})
  .map((data) => data.value)
  .recover((err) => "error occurred")
  .toPromise();

Pattern Matching

import {
  match,
  when,
  otherwise,
  extract,
  value,
  array,
  or,
  and,
  not,
  type as matchType,
  object,
} from "@chris5855/scats";

// Simple value matching
const result = match(42)
  .with(1, () => "one")
  .with(2, () => "two")
  .with(
    when((n) => n > 10),
    (n) => `greater than 10: ${n}`
  )
  .otherwise(() => "default case")
  .run();

// Object pattern matching
const person = { name: "John", age: 30 };
const greeting = match(person)
  .with(
    object({ name: "John", age: when<number>((a) => a > 18) }),
    () => "Hello Mr. John"
  )
  .with(object({ name: "John" }), () => "Hello John")
  .otherwise(() => "Hello stranger")
  .run();

// Advanced pattern matching
// Extract pattern
match(person)
  .with(
    extract((p) => p.name),
    (name) => `Name is: ${name}`
  )
  .otherwise(() => "No match")
  .run();

// Array pattern
match([1, 2, 3])
  .with(array([1, 2, 3]), () => "exact match")
  .with(array([1, when((n) => n > 1), 3]), () => "pattern match")
  .otherwise(() => "no match")
  .run();

// Combining patterns with or, and, not
match(42)
  .with(or(value(41), value(42), value(43)), () => "one of 41, 42, 43")
  .with(
    and(
      when((n) => n > 40),
      when((n) => n < 50)
    ),
    () => "between 40 and 50"
  )
  .with(not(value(100)), () => "anything but 100")
  .otherwise(() => "no match")
  .run();

// Type matching
class Cat {}
class Dog {}
match(new Cat())
  .with(matchType(Cat), () => "It's a cat")
  .with(matchType(Dog), () => "It's a dog")
  .otherwise(() => "unknown animal")
  .run();

Immutable Collections

import { List, Map, Set, ArraySeq, ArrayBuffer } from "@chris5855/scats";

// List
const numbers = List.of(1, 2, 3, 4, 5);
const doubled = numbers.map((n) => n * 2);
const sum = numbers.foldLeft(0, (acc, n) => acc + n);
const evens = numbers.filter((n) => n % 2 === 0);

// Map
const userMap = Map.of([
  ["user1", { name: "Alice", age: 25 }],
  ["user2", { name: "Bob", age: 30 }],
]);
const hasUser = userMap.has("user1");
const olderUsers = userMap.filter((user) => user.age > 25);

// Set
const uniqueNumbers = Set.of(1, 2, 3, 2, 1);
const union = uniqueNumbers.union(Set.of(3, 4, 5));
const intersection = uniqueNumbers.intersection(Set.of(2, 3, 4));
const difference = uniqueNumbers.difference(Set.of(2, 3));

// ArraySeq (IndexedSeq)
const seq = new ArraySeq([1, 2, 3, 4, 5]);
const mappedSeq = seq.map((x) => x * 2);
console.log(Array.from(mappedSeq).join(", ")); // "2, 4, 6, 8, 10"

// ArrayBuffer (mutable Buffer)
const buffer = new ArrayBuffer<number>();
buffer.append(1).append(2).append(3);
console.log(Array.from(buffer).join(", ")); // "1, 2, 3"
buffer.prepend(0);
console.log(Array.from(buffer).join(", ")); // "0, 1, 2, 3"

LazyList

import { LazyList } from "@chris5855/scats";

// Creating a LazyList
const numbers = LazyList.of(1, 2, 3, 4, 5);

// Creating an infinite sequence of numbers
const naturals = LazyList.from(1).iterate((n) => n + 1);

// Taking only what you need
const first10 = naturals.take(10).toArray(); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// Lazy transformation
const evenSquares = naturals
  .filter((n) => n % 2 === 0) // Only even numbers
  .map((n) => n * n) // Square them
  .take(5) // Take first 5
  .toArray(); // [4, 16, 36, 64, 100]

// Generate a range of numbers
const range = LazyList.range(1, 10); // 1 to 9

// Create from iterable
const fromArray = LazyList.from([1, 2, 3]);

// Creating an infinite stream with a generator function
const randomNumbers = LazyList.continually(() => Math.random())
  .take(3)
  .toArray(); // Three random numbers

// Iterate from a seed value
const powers = LazyList.iterate(1, (n) => n * 2)
  .take(5)
  .toArray(); // [1, 2, 4, 8, 16]

Vector

import { Vector } from "@chris5855/scats";

// Creating a Vector
const vec = Vector.of(1, 2, 3, 4, 5);

// Accessing elements (constant time)
const third = vec.apply(2); // 3
const maybeValue = vec.get(10); // None (out of bounds)

// Modifying elements
const updated = vec.updated(2, 10); // Vector(1, 2, 10, 4, 5)

// Adding elements
const appended = vec.appended(6); // Vector(1, 2, 3, 4, 5, 6)
const prepended = vec.prepended(0); // Vector(0, 1, 2, 3, 4, 5)

// Combining vectors
const combined = vec.appendAll(Vector.of(6, 7, 8)); // Vector(1, 2, 3, 4, 5, 6, 7, 8)

// Transforming vectors
const doubled = vec.map((n) => n * 2); // Vector(2, 4, 6, 8, 10)
const even = vec.filter((n) => n % 2 === 0); // Vector(2, 4)

// Flattening
const vectors = Vector.of(Vector.of(1, 2), Vector.of(3, 4));
const flattened = vectors.flatMap((v) => v); // Vector(1, 2, 3, 4)

// Static constructors
const empty = Vector.empty<number>(); // Empty vector
const fromArray = Vector.from([1, 2, 3]); // Vector from array

For-Comprehensions

import {
  For,
  Some,
  None,
  List,
  ForComprehensionBuilder,
  Monad,
} from "@chris5855/scats";

// Option comprehension
const optionResult = For.option<{ a: number; b: number; c: number }>()
  .bind("a", () => Some(1))
  .bind("b", ({ a }) => Some(a + 1))
  .bind("c", ({ a, b }) => Some(a + b))
  .yield(({ a, b, c }) => a + b + c); // Some(6)

// List comprehension
const matrix = For.list<{ row: number; col: string }>()
  .bind("row", () => List.of(1, 2, 3))
  .bind("col", () => List.of("A", "B"))
  .yield(({ row, col }) => `${row}${col}`); // List(1A, 1B, 2A, 2B, 3A, 3B)

// Custom monad example
class Identity<A> implements Monad<A> {
  constructor(private readonly value: A) {}

  map<B>(f: (a: A) => B): Identity<B> {
    return new Identity(f(this.value));
  }

  flatMap<B>(f: (a: A) => Identity<B>): Identity<B> {
    return f(this.value);
  }

  get(): A {
    return this.value;
  }

  static of<A>(a: A): Identity<A> {
    return new Identity(a);
  }
}

// Creating a custom comprehension
const builder = new ForComprehensionBuilder<Identity<any>, {}>();
const idComp = builder.custom(
  <A>(a: A) => Identity.of(a),
  <A>(ma: Identity<A>, f: (a: A) => Identity<any>) => ma.flatMap(f)
);

const result = idComp
  .bind("x", () => Identity.of(10))
  .bind("y", (env) => Identity.of((env as any).x * 2))
  .yield((env) => (env as any).x + (env as any).y); // Identity(30)

Type Classes

import {
  TypeClass,
  TypeClassRegistry,
  register,
  extension,
  withContext,
} from "@chris5855/scats";

// Define a type class
interface Numeric<T> extends TypeClass<T> {
  add(a: T, b: T): T;
  zero(): T;
}

// Create instances for different types
const numberNumeric: Numeric<number> = {
  __type: undefined as any as number,
  add: (a, b) => a + b,
  zero: () => 0,
};

const stringNumeric: Numeric<string> = {
  __type: undefined as any as string,
  add: (a, b) => a + b,
  zero: () => "",
};

// Register instances in a registry
const registry = new TypeClassRegistry<Numeric<any>>();
registry.register(numberNumeric, Number);
registry.register(stringNumeric, String);

// Using the registry
function sum<T>(values: T[], registry: TypeClassRegistry<Numeric<any>>): T {
  if (values.length === 0) throw new Error("Cannot sum empty array");
  const numeric = registry.getFor(values[0]);
  return values.reduce((acc, val) => numeric.add(acc, val), numeric.zero());
}

// Example usage
console.log(sum([1, 2, 3, 4], registry)); // 10
console.log(sum(["a", "b", "c"], registry)); // "abc"

// Using extension methods
const getNumberValue = (value: any) => value as number;
const addMethod = extension<number, Numeric<number>>(
  registry,
  getNumberValue
)("add");

// Using context bounds
withContext<number, Numeric<number>>(registry, (numeric) => {
  const result = numeric.add(5, 10);
  console.log(result); // 15
});

// Using the registry directly
const numeric = registry.getFor(5);
console.log(`Add with type class: ${numeric.add(5, 10)}`); // Add with type class: 15

Tuples

import { Tuple, Tuple2, Tuple3 } from "@chris5855/scats";

// Creating tuples
const pair = Tuple.of(1, "hello");
const triple = Tuple.of(1, "hello", true);

// Accessing elements
const first = pair._1; // 1
const second = pair._2; // "hello"
const third = triple._3; // true

// Destructuring
const [num, str] = pair;
const [x, y, z] = triple;

// Tuple operations
const swapped = pair.swap(); // Tuple2("hello", 1)
const mappedFirst = pair.map1((n) => n * 2); // Tuple2(2, "hello")
const mappedSecond = pair.map2((s) => s.toUpperCase()); // Tuple2(1, "HELLO")
const mapped = triple.map(
  (n) => n * 2,
  (s) => s.toUpperCase(),
  (b) => !b
); // Tuple3(2, "HELLO", false)

// Creating tuples from arrays
const pairFromArray = Tuple.fromArray2([1, "hello"]);
const tripleFromArray = Tuple.fromArray3([1, "hello", true]);

// Creating a tuple from a Map entry
const entry: [string, number] = ["key", 123];
const entryTuple = Tuple.fromEntry(entry); // Tuple2("key", 123)

Ordering

import { Ordering } from "@chris5855/scats";

// Using built-in orderings
const numbers = [3, 1, 4, 1, 5, 9];
const sortedNumbers = [...numbers].sort((a, b) =>
  Ordering.number.compare(a, b)
);
// sortedNumbers is [1, 1, 3, 4, 5, 9]

const strings = ["banana", "apple", "cherry"];
const sortedStrings = [...strings].sort((a, b) =>
  Ordering.string.compare(a, b)
);
// sortedStrings is ["apple", "banana", "cherry"]

// Finding min/max values
const min = Ordering.number.min(10, 5); // 5
const max = Ordering.number.max(10, 5); // 10

// Creating a custom ordering
type Person = { name: string; age: number };
const people = [
  { name: "Alice", age: 30 },
  { name: "Bob", age: 25 },
  { name: "Charlie", age: 35 },
];

// Order by age
const byAge = Ordering.by<Person, number>((p) => p.age);
const sortedByAge = [...people].sort((a, b) => byAge.compare(a, b));
// First person is Bob (age 25)

// Order by name
const byName = Ordering.by<Person, string>((p) => p.name);
const sortedByName = [...people].sort((a, b) => byName.compare(a, b));
// First person is Alice

// Reverse ordering
const descendingOrder = Ordering.number.reverse();
const sortedDesc = [...numbers].sort((a, b) => descendingOrder.compare(a, b));
// sortedDesc is [9, 5, 4, 3, 1, 1]

// Chaining orderings (e.g., sort by lastname, then by firstname)
const byLastname = Ordering.by<Person, string>((p) => p.lastname);
const byFirstname = Ordering.by<Person, string>((p) => p.firstname);
const byLastThenFirst = byLastname.andThen(byFirstname);

Resource Management

import { Using, Closeable, Success } from "@chris5855/scats";

// Create a resource that needs to be closed
class Resource implements Closeable {
  constructor(readonly id: string) {
    console.log(`Resource ${id} created`);
  }

  getData(): string {
    return `Data from resource ${this.id}`;
  }

  close(): void {
    console.log(`Resource ${id} closed`);
  }
}

// Use a single resource and ensure it gets closed
const result = Using.resource(new Resource("123"), (resource) => {
  return resource.getData().toUpperCase();
});
// Result is: Success(DATA FROM RESOURCE 123)
// resource.close() is guaranteed to be called, even if an exception is thrown

// Using multiple resources
const multiResult = Using.resources(
  [new Resource("A"), new Resource("B")],
  ([resourceA, resourceB]) => {
    return resourceA.getData() + " + " + resourceB.getData();
  }
);
// Result is: Success(Data from resource A + Data from resource B)
// Both resources are closed in reverse order (B then A)

State Monad

import { State } from "@chris5855/scats";

// Simple counter using State monad
const increment = State.modify<number>((n) => n + 1);
const getCount = State.get<number>();

// Combining state operations
const counter = increment
  .flatMap(() => increment)
  .flatMap(() => increment)
  .flatMap(() => getCount);

const [count, finalState] = counter.run(0);
// count: 3, finalState: 3

// More complex example: implementing a stack
type Stack = number[];

// Define stack operations
const push = (n: number) => State.modify<Stack>((stack) => [...stack, n]);
const pop = State.modify<Stack>((stack) => {
  const newStack = [...stack];
  newStack.pop();
  return newStack;
});
const peek = State.gets<Stack, number | undefined>(
  (stack) => stack[stack.length - 1]
);

// Use the operations in a computation
const stackOperations = push(1)
  .flatMap(() => push(2))
  .flatMap(() => push(3))
  .flatMap(() => peek)
  .flatMap((top) => pop.map(() => top));

const [topValue, resultStack] = stackOperations.run([]);
// topValue: 3, resultStack: [1, 2]

// Using eval and exec
const result = State.of<number, string>("hello").eval(42); // "hello"
const newState = State.put<number>(100).exec(42); // 100

Writer Monad

import { Writer, Monoids } from "@chris5855/scats";

// Simple logging with Writer monad
const logNumber = (n: number) =>
  Writer.tell<string>(`Processing ${n}`).flatMap(
    () => Writer.of(n * 2, Monoids.string),
    Monoids.string
  );

const result = logNumber(5).flatMap(
  (n) =>
    Writer.tell<string>(`Result is ${n}`).flatMap(
      () => Writer.of(n, Monoids.string),
      Monoids.string
    ),
  Monoids.string
);

const [value, logs] = result.run();
// value: 10, logs: "Processing 5Result is 10"

// Using array logs for structured logging
type StringArray = string[];

const add = (n: number) => Writer.withArray<string, number>(n, [`add(${n})`]);

const calculation = add(5)
  .flatMap(
    (n) =>
      add(10).flatMap(
        (m) => Writer.withArray<string, number>(n + m, [`sum(${n}, ${m})`]),
        Monoids.array<string>()
      ),
    Monoids.array<string>()
  )
  .flatMap(
    (n) => Writer.withArray<string, number>(n * 2, [`double(${n})`]),
    Monoids.array<string>()
  );

const [calcResult, calcLogs] = calculation.run();
// calcResult: 30, calcLogs: ['add(5)', 'add(10)', 'sum(5, 10)', 'double(15)']

:handshake: Contributing

  • Fork it!
  • Create your feature branch: git checkout -b my-new-feature
  • Commit your changes: git commit -am 'Add some feature'
  • Push to the branch: git push origin my-new-feature
  • Submit a pull request

:busts_in_silhouette: Credits


:anger: Troubleshootings

This is just a personal project created for study / demonstration purpose and to simplify my working life, it may or may not be a good fit for your project(s).


:heart: Show your support

Please :star: this repository if you like it or this project helped you!
Feel free to open issues or submit pull-requests to help me improving my work.


:robot: Author

Chris M. Perez

You can follow me on github · twitter


Copyright ©2025 scats.