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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nicolastoulemont/std

v0.7.2

Published

Collection of utility functions

Readme

@nicolastoulemont/std

Introduction

@nicolastoulemont/std is a functional TypeScript toolkit for modeling domain data, handling failures explicitly, and composing sync or async workflows. It is designed for application code where clear control flow, predictable typing, and dependency-aware orchestration matter. The API is small, pipe-friendly, and built around practical primitives you can combine incrementally.

Installation

pnpm add @nicolastoulemont/std

Quick Start

import { Result, Data, pipe } from "@nicolastoulemont/std"

class InvalidPortError extends Data.TaggedError("InvalidPortError")<{ input: string }> {}

const parsePort = (input: string) =>
  pipe(
    Result.try(() => Number.parseInt(input, 10)),
    Result.filter(
      (n) => Number.isInteger(n) && n > 0,
      () => new InvalidPortError({ input }),
    ),
  )
import { Fx, Layer, Provide, Service, pipe } from "@nicolastoulemont/std"

const Config = Service.tag<{ baseUrl: string }>("Config")

const ConfigLive = Layer.ok(Config, { baseUrl: "https://api.example.com" })

const program = Fx.gen(function* () {
  const config = yield* Config
  return config.baseUrl
})

const exit = Fx.run(pipe(program, Provide.layer(ConfigLive)))

const response = Fx.match(exit, {
  Ok: (ok) => ({ status: 200, body: ok.value }),
  Err: (err) => ({ status: 400, body: err.error }),
  Defect: (defect) => ({ status: 500, body: String(defect.defect) }),
})

Main Modules

Result

Result models success/failure with typed errors so transformations stay explicit and composable.

Abstract Example

import { Result, Data, pipe } from "@nicolastoulemont/std"

class NotPositiveIntegerError extends Data.TaggedError("NotPositiveIntegerError")<{ input: string }> {}

const parsePositiveInt = (input: string) => {
  const parsed = Number.parseInt(input, 10)
  return pipe(
    Result.ok(parsed),
    Result.filter(
      (n) => Number.isInteger(n) && n > 0,
      () => new NotPositiveIntegerError({ input }),
    ),
  )
}

Real-World Example

import { Result, Data, pipe } from "@nicolastoulemont/std"

class ValidationError extends Data.TaggedError("ValidationError")<{ message: string }> {}
class ConflictError extends Data.TaggedError("ConflictError")<{ message: string }> {}
type SignupError = ValidationError | ConflictError

const validateEmail = (email: string) =>
  email.includes("@") ? Result.ok(email) : Result.err<SignupError>(new ValidationError({ message: "Invalid email" }))

const createUser = (email: string) =>
  email === "[email protected]"
    ? Result.err<SignupError>(new ConflictError({ message: "Email already used" }))
    : Result.ok({ id: "u_123", email })

const signup = (email: string) => pipe(validateEmail(email), Result.flatMap(createUser))

Option

Option models optional presence/absence when missing data is expected and not an error condition.

Abstract Example

import { Option, pipe } from "@nicolastoulemont/std"

const normalizedName = (value: string | undefined) =>
  pipe(
    Option.fromNullable(value),
    Option.map((name) => name.trim()),
    Option.filter((name) => name.length > 0),
    Option.unwrapOr("Anonymous"),
  )

Real-World Example

import { Option, pipe } from "@nicolastoulemont/std"

const readPagination = (query: URLSearchParams) => ({
  page: pipe(
    Option.fromNullable(query.get("page")),
    Option.map((raw) => Number.parseInt(raw, 10)),
    Option.filter((n) => Number.isInteger(n) && n > 0),
    Option.unwrapOr(1),
  ),
  limit: pipe(
    Option.fromNullable(query.get("limit")),
    Option.map((raw) => Number.parseInt(raw, 10)),
    Option.filter((n) => Number.isInteger(n) && n > 0 && n <= 100),
    Option.unwrapOr(20),
  ),
})

Either

Either models two valid branches where both sides are meaningful outcomes rather than success versus failure.

Abstract Example

import { Either } from "@nicolastoulemont/std"

const parseSource = (input: "local" | "remote") => (input === "local" ? Either.left("LOCAL") : Either.right("REMOTE"))

const label = Either.match(parseSource("local"), {
  Left: (source) => `Source: ${source}`,
  Right: (source) => `Source: ${source}`,
})

Real-World Example

import { Either, pipe } from "@nicolastoulemont/std"

type Source = "cache" | "database"
type User = { id: string; name: string }

const findUser = (id: string) =>
  id.startsWith("cached:") ? Either.left<Source, User>("cache") : Either.right<Source, User>({ id, name: "Ada" })

const responseMeta = (id: string) =>
  pipe(
    findUser(id),
    Either.match({
      Left: (source) => ({ source, stale: true }),
      Right: (user) => ({ source: "database" as const, stale: false, user }),
    }),
  )

Fx

Fx models generator-based effects with typed dependencies and short-circuiting typed failures.

Abstract Example

import { Fx, Layer, Provide, Service, pipe } from "@nicolastoulemont/std"

const Clock = Service.tag<{ now: () => number }>("Clock")
const ClockLive = Layer.ok(Clock, { now: () => Date.now() })

const program = Fx.gen(function* () {
  const clock = yield* Clock
  return clock.now()
})

const exit = Fx.run(pipe(program, Provide.layer(ClockLive)))

const timestamp = Fx.match(exit, {
  Ok: (ok) => ok.value,
  Err: () => 0,
  Defect: () => 0,
})

Real-World Example

import { Fx, Layer, Result, Data, Provide, Service, pipe } from "@nicolastoulemont/std"

const Api = Service.tag<{ postOrder: (input: { sku: string; qty: number }) => Promise<{ orderId: string }> }>("Api")
const ApiLive = Layer.ok(Api, {
  postOrder: async () => ({ orderId: "ord_42" }),
})

class InvalidQuantityError extends Data.TaggedError("InvalidQuantityError")<{ qty: number }> {}

const submitOrder = Fx.gen(function* (payload: { sku?: string; qty: number }) {
  const api = yield* Api
  const sku = yield* Fx.option(payload.sku)
  const validQty = yield* Result.filter(
    Result.ok(payload.qty),
    (qty) => qty > 0,
    (qty) => new InvalidQuantityError({ qty }),
  )
  return yield* Fx.try(() => api.postOrder({ sku, qty: validQty }))
})

const exit = Fx.run(pipe(submitOrder({ sku: "book-1", qty: 2 }), Provide.layer(ApiLive)))

const httpResponse = Fx.match(exit, {
  Ok: (ok) => ({ status: 201, body: ok.value }),
  Err: (err) => ({ status: 400, body: String(err.error) }),
  Defect: () => ({ status: 500, body: "Unexpected defect" }),
})

Retry Example

import { Fx, Result, Schedule } from "@nicolastoulemont/std"

let attempts = 0

const flaky = Fx.gen(function* () {
  attempts += 1
  if (attempts < 3) {
    return yield* Result.err("temporary" as const)
  }
  return "ok"
})

const exit = Fx.run(Fx.retry(flaky, Schedule.recurs(5)))

Nested Retry with Dependencies

import { Fx, Layer, Result, Schedule, pipe, Provide, Service } from "@nicolastoulemont/std"

type ConfigService = { baseUrl: string }
const Config = Service.tag<ConfigService>("Config")
const ConfigLive = Layer.ok(Config, { baseUrl: "https://api.example.com" })

let attempts = 0

const inner = Fx.retry(
  Fx.gen(function* () {
    const config = yield* Config
    attempts += 1
    if (attempts < 2) {
      return yield* Result.err("temporary" as const)
    }
    return config.baseUrl
  }),
  Schedule.recurs(2),
)

const program = Fx.gen(function* () {
  const baseUrl = yield* inner
  return `ready:${baseUrl}`
})

const exit = Fx.run(pipe(program, Provide.layer(ConfigLive)))

Concurrent Traversal with Fx.forEach

import { Fx } from "@nicolastoulemont/std"

const loadUsers = Fx.forEach(
  ["u1", "u2", "u3"],
  (id) =>
    Fx.gen(async function* () {
      const response = await fetch(`/api/users/${id}`)
      return yield* Fx.try(() => response.json())
    }),
  { concurrency: 2 },
)

const exit = await Fx.run(loadUsers)

Queue

Queue provides a standalone FIFO task queue with configurable concurrency, backpressure (bounded mode), and lifecycle controls.

Abstract Example

import { Queue } from "@nicolastoulemont/std"

const queue = Queue.make({ concurrency: 2 })

const first = queue.enqueue(() => 1)
const second = queue.enqueue(async () => 2)

await queue.awaitIdle()
await queue.shutdown({ mode: "drain" })

Real-World Example

import { Queue } from "@nicolastoulemont/std"

const imageQueue = Queue.bounded(100, { concurrency: 4 })

const tasks = imageUrls.map((url) =>
  imageQueue.enqueue(async ({ signal }) => {
    const response = await fetch(url, { signal })
    return response.arrayBuffer()
  }),
)

const buffers = await Promise.all(tasks)
await imageQueue.shutdown({ mode: "drain" })

Multithread

Multithread runs self-contained callbacks in worker threads using a Result-first API while remaining yieldable in Fx.gen.

Abstract Example

import { Multithread } from "@nicolastoulemont/std"

const op = Multithread.run((input: string, ctx) => {
  ctx.throwIfCancelled()
  return input.toUpperCase()
}, "hello")

const result = await op.result()

Real-World Example

import { Fx, Multithread } from "@nicolastoulemont/std"

const program = Fx.gen(async function* () {
  const records = yield* Multithread.map(
    ['{"id":"1","email":"[email protected]"}', '{"id":"2","email":"[email protected]"}'],
    (line, _index, ctx) => {
      ctx.throwIfCancelled()
      try {
        return JSON.parse(line) as { id: string; email: string }
      } catch {
        return { _tag: "Err" as const, error: { _tag: "ParseError" as const, line } }
      }
    },
    { parallelism: 4 },
  )

  const preferred = yield* Multithread.firstSuccess([Multithread.run(() => "cache"), Multithread.run(() => "database")])

  return { records, preferred }
})

const exit = await Fx.run(program)

Multithread cancellation is cooperative. abort() always cancels logically, and worker code can stop early by calling ctx.throwIfCancelled().

Adt

Adt provides schema-backed tagged variants so you can model domain state with exhaustive pattern matching.

Abstract Example

import { Adt, type AdtInfer } from "@nicolastoulemont/std"
import { z } from "zod"

const Shape = Adt.union("Shape", {
  Circle: z.object({ radius: z.number() }),
  Square: z.object({ side: z.number() }),
})
type Shape = AdtInfer<typeof Shape>

const describeShape = (shape: Shape) =>
  Adt.match(shape, {
    Circle: (value) => `circle(${value.radius})`,
    Square: (value) => `square(${value.side})`,
  })

Real-World Example

import { Adt, type AdtInfer } from "@nicolastoulemont/std"
import { z } from "zod"

const OrderState = Adt.union("OrderState", {
  Draft: z.object({ id: z.string() }),
  Confirmed: z.object({ id: z.string(), paymentId: z.string() }),
  Shipped: z.object({ id: z.string(), trackingId: z.string() }),
})
type OrderState = AdtInfer<typeof OrderState>

const badgeLabel = (state: OrderState) =>
  Adt.match(state, {
    Draft: () => "Waiting for payment",
    Confirmed: () => "Preparing shipment",
    Shipped: (value) => `Shipped: ${value.trackingId}`,
  })

Data

Data creates immutable structural value objects (Data.struct, Data.tuple, Data.array, Data.tagged) with stable equality and hashing semantics.

Abstract Example

import { Data } from "@nicolastoulemont/std"

const a = Data.struct({ env: "prod", retries: 3 })
const b = Data.struct({ env: "prod", retries: 3 })

const same = a.equals(b) // true

Real-World Example

import { Data } from "@nicolastoulemont/std"

const previous = Data.struct({ search: "books", sort: "price-asc" })
const next = Data.struct({ search: "books", sort: "price-asc" })

if (previous.equals(next)) {
  // Skip redundant fetch because filter state is structurally identical
}

Order

Order provides composable comparators and immutable sorting helpers.

Abstract Example

import { Order, pipe } from "@nicolastoulemont/std"

type User = { name: string; age: number }

const byAge = Order.by(Order.number, (user: User) => user.age)
const byName = Order.by(Order.string, (user: User) => user.name)

const userOrder = Order.merge(byAge, byName)
const sameOrder = pipe(byAge, Order.merge(byName))
const allOrders = Order.merge([byAge, byName])

const sorted = Order.sort(
  [
    { name: "bob", age: 30 },
    { name: "alice", age: 30 },
    { name: "zoe", age: 25 },
  ],
  allOrders,
)

Real-World Example

import { Order } from "@nicolastoulemont/std"

type Product = {
  id: string
  category: string
  price: number
  rating: number
}

const byCategory = Order.by(Order.string, (product: Product) => product.category)
const byPrice = Order.by(Order.number, (product: Product) => product.price)
const byRatingDesc = Order.reverse(Order.by(Order.number, (product: Product) => product.rating))

const sortProducts = Order.sortBy(byCategory, byPrice, byRatingDesc)

const products: Product[] = [
  { id: "a", category: "books", price: 20, rating: 4.8 },
  { id: "b", category: "books", price: 20, rating: 4.5 },
  { id: "c", category: "games", price: 60, rating: 4.7 },
]

const sorted = sortProducts(products)

pipe / flow

pipe and flow compose sync/async transformations into readable, type-inferred data pipelines.

Abstract Example

import { pipe, flow } from "@nicolastoulemont/std"

const toLabel = flow(
  (n: number) => n * 2,
  (n) => n.toString(),
  (s) => `value:${s}`,
)

const result = pipe(10, (n) => n + 1, toLabel) // "value:22"

Real-World Example

import { pipe } from "@nicolastoulemont/std"

type RawProfile = { name?: string; age?: string }

const normalizeProfile = (input: RawProfile) =>
  pipe(
    input,
    (p) => ({ name: p.name?.trim() ?? "", age: Number.parseInt(p.age ?? "0", 10) }),
    (p) => ({ ...p, age: Number.isNaN(p.age) ? 0 : p.age }),
    (p) => ({ ...p, isAdult: p.age >= 18 }),
  )