A comprehensive TypeScript library bringing Scala's powerful functional programming paradigms to JavaScript/TypeScript, featuring immutable collections, monads, pattern matching, and more
- 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
npm install @chris5855/scats
yarn add @chris5855/scats
pnpm add @chris5855/scats
# Install dependencies
npm install
# Build the project
npm run build
# Run examples
npm run example
# Run tests
npm run test
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);
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}`
);
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();
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();
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"
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]
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
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)
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
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)
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);
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)
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
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)']
- 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
- Chris Michael (Project Leader, and Developer)
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).
Please ⭐ 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.
Chris M. Perez
Copyright ©2025 scats.