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

p-lens

v1.0.3

Published

**Composable, type-safe pattern matching and runtime validation — with zero dependencies and full TypeScript inference.**

Readme

p-lens

Composable, type-safe pattern matching and runtime validation — with zero dependencies and full TypeScript inference.

No more as unknown as YourType. No more hand-rolled type guards. No more any assertions at API boundaries. The matching module gives you a single, expressive API to describe the shape of your data, validate it at runtime, and have TypeScript automatically infer the correct type — all in one shot.


Quick start

import { pattern, find } from "p-lens";

const UserPattern = pattern.mapping({
  name: pattern.string(),
  age: pattern.anno.min(pattern.number(), 18),
  role: pattern.union(pattern.literal("admin"), pattern.literal("user")),
  nickname: pattern.anno.optional(pattern.string()),
});

// type guard — narrows unknown → inferred type
if (pattern.is.matching(data, UserPattern)) {
  console.log(data.name); // TypeScript knows this is a string
}

// validated result with error path on failure
const result = pattern.is.validated(data, UserPattern);
if (!result.ok) {
  console.error(result.path, result.reason);
}

Why this?

  • Zero schema duplication. Define once, get both runtime validation and compile-time types.
  • Precise error reporting. Failures include a path (e.g. ["user", "address", "zip"]) so you know exactly where things went wrong.
  • Fuzzy matching built in. Validate "close enough" strings and numbers using Jaro-Winkler similarity — perfect for search, AI output parsing, and user input normalization.
  • Deep search with find(). Extract typed values from arbitrarily nested objects, arrays, Maps, and Sets in a single generator call.
  • Phantom types throughout. No casts needed — the inferred TypeScript type flows through every composition operation automatically.

Primitives

The fundamental building blocks. Each returns a pattern that both validates at runtime and informs TypeScript of the correct type.

pattern.string()   // string
pattern.number()   // number
pattern.boolean()  // boolean
pattern.date()     // Date
pattern.nil()      // null     (strict — undefined does NOT match)
pattern.none()     // undefined
pattern.unknown()  // unknown  (always passes)

Literals

Match an exact value. TypeScript infers the literal type.

pattern.literal("admin")           // type: "admin"
pattern.literal(42)                // type: 42
pattern.literal(true)              // type: true
pattern.literal(new Date("2025-01-01"))  // type: Date (exact timestamp)

// or the explicit variants:
pattern.literalString("hello")
pattern.literalNumber(0)
pattern.literalBoolean(false)
pattern.literalDate(someDate)

Regex

Match strings against a regular expression. Full regex flag support — case-insensitive, multiline, whatever you need. On failure the error message includes the regex so you know what was expected.

const Digits = pattern.regex(/^\d+$/);
const Slug = pattern.regex(/^[a-z0-9-]+$/i);

pattern.is.matching("123", Digits)  // true
pattern.is.matching("abc", Digits)  // false

Fuzzy matching

When "exactly equal" is too strict, fuzzy patterns let you express intent rather than precision. Both string and number variants are supported.

Fuzzy strings — Jaro-Winkler similarity

// Default threshold: 0.8
const Hello = pattern.fuzzyString("hello");
pattern.is.matching("hello", Hello)  // true
pattern.is.matching("helo",  Hello)  // true  — close enough
pattern.is.matching("world", Hello)  // false

// Tune the threshold
const Strict = pattern.fuzzyString("hello", 0.99);
const Lenient = pattern.fuzzyString("hello", 0.5);

// Case-insensitive matching
const ICase = pattern.fuzzyString("Hello", { threshold: 0.8, icase: true });

Fuzzy numbers — proximity score

Uses a 1 / (1 + |target - data|) score so values close to the target pass and outliers fail.

const Near10 = pattern.fuzzyNumber(10, 0.8);
pattern.is.matching(9.99, Near10)    // true
pattern.is.matching(903.2, Near10)  // false

Shorthand: pattern.like

like dispatches to the right fuzzy variant based on the target type:

pattern.like("foo", 0.8)    // → fuzzyString
pattern.like(10, 0.8)       // → fuzzyNumber

Composite patterns

Arrays

const Numbers = pattern.array(pattern.number());
// inferred type: number[]

const Matrix = pattern.array(pattern.array(pattern.boolean()));
// inferred type: boolean[][]

Mappings (objects)

Describe the shape of a plain object. Extra keys are allowed and ignored. Missing required keys produce a failure with the exact key in the error path.

const Point = pattern.mapping({
  x: pattern.number(),
  y: pattern.number(),
});
// inferred type: { x: number; y: number }

Unions

const StringOrNumber = pattern.union(pattern.string(), pattern.number());
// inferred type: string | number

// Discriminated unions work naturally with literals
const Event = pattern.union(
  pattern.mapping({ type: pattern.literal("click"), x: pattern.number(), y: pattern.number() }),
  pattern.mapping({ type: pattern.literal("keydown"), key: pattern.string() }),
);
// inferred type: { type: "click"; x: number; y: number } | { type: "keydown"; key: string }

When a union fails, the reasons from all branches are collected and returned together so you understand every path that was tried.

Tuples

Fixed-length arrays where each position has its own type.

const Coord = pattern.tuple(pattern.number(), pattern.number());
// inferred type: [number, number]

const Row = pattern.tuple(
  pattern.string(),
  pattern.number(),
  pattern.boolean(),
);
// inferred type: [string, number, boolean]

Annotations

Annotations layer metadata and constraints onto any pattern without changing the core type. They compose cleanly with all other patterns.

optional — mark a mapping field as optional

const User = pattern.mapping({
  name: pattern.string(),
  nickname: pattern.anno.optional(pattern.string()),
});
// inferred type: { name: string; nickname?: string }

min / max — range constraints

Works on strings (length), numbers (value), and dates (timestamp).

const Username = pattern.anno.min(pattern.anno.max(pattern.string(), 20), 3);
// must be 3–20 characters

const Age = pattern.anno.min(pattern.number(), 0);
const Score = pattern.anno.minmax(pattern.number(), 0, 100);

const RecentDate = pattern.anno.min(pattern.date(), Date.now() - 86400_000);

title / description — documentation metadata

const Email = pattern.anno.titled(
  pattern.anno.described(pattern.string(), "A valid email address"),
  "Email",
);

annotated — apply multiple annotations at once

const Percentage = pattern.anno.annotated(pattern.number(), {
  title: "Percentage",
  min: 0,
  max: 100,
});

Validation API

pattern.is.matching(data, pat) — type guard

Returns a boolean. Narrows unknown to the inferred type of the pattern.

const data: unknown = fetchSomething();

if (pattern.is.matching(data, User)) {
  // data is { name: string; age: number; ... } here
}

pattern.is.validated(data, pat) — detailed result

Returns a discriminated union with full error information on failure.

const result = pattern.is.validated(data, User);

if (result.ok) {
  console.log(result.value); // typed as the pattern's inferred type
} else {
  console.error({
    path:   result.path,    // string[] — e.g. ["user", "address", "zip"]
    reason: result.reason,  // string[] — human-readable explanation(s)
  });
}

The path makes errors pinpoint-accurate. For deeply nested schemas you'll know exactly which field caused the failure without having to guess.


find() — deep search

This is where things get exciting. find traverses an arbitrarily nested data structure — objects, arrays, Maps, Sets, any combination — and yields every value that matches the given pattern. It's a generator, so results are lazy and you can stop early.

import { find } from "p-lens";

const data = {
  users: [
    { name: "Alice", role: "admin" },
    { name: "Bob",   role: "user" },
  ],
  config: {
    owner: { name: "Charlie", role: "admin" },
  },
};

const AdminPattern = pattern.mapping({
  name: pattern.string(),
  role: pattern.literal("admin"),
});

for (const admin of find(data, AdminPattern)) {
  console.log(admin.name); // "Alice", "Charlie" — fully typed
}

find traverses into:

  • Plain objects — recurses into all values
  • Arrays — recurses into all elements
  • Map — recurses into all values
  • Set — recurses into all values

It does not require you to know the structure ahead of time. Throw an entire API response, a config blob, or a parsed document at it and let the pattern do the work.

Real-world example: parsing AI output

const ToolCall = pattern.mapping({
  tool: pattern.string(),
  args: pattern.mapping({
    query: pattern.string(),
  }),
});

// response from an LLM might bury tool calls anywhere in the structure
for (const call of find(llmResponse, ToolCall)) {
  await dispatch(call.tool, call.args.query);
}

Stop early with break

Because find is a generator you get lazy evaluation for free:

const [first] = find(data, SomePattern);  // only traverses until the first match

Type inference

Every pattern carries its output type through a phantom type parameter, so InferPattern<P> gives you the TypeScript type without any casts:

import type { InferPattern } from "p-lens";

const UserPattern = pattern.mapping({
  name: pattern.string(),
  age:  pattern.number(),
});

type User = InferPattern<typeof UserPattern>;
// { name: string; age: number }

Optional fields, unions, tuples, literals — all flow through correctly. Define the pattern once and let the types derive themselves.


Putting it all together

const ApiResponse = pattern.mapping({
  status: pattern.union(pattern.literal("ok"), pattern.literal("error")),
  data: pattern.anno.optional(
    pattern.array(
      pattern.mapping({
        id:    pattern.anno.min(pattern.number(), 1),
        label: pattern.anno.minmax(pattern.string(), 1, 100),
        tags:  pattern.array(pattern.string()),
      })
    )
  ),
  error: pattern.anno.optional(pattern.string()),
});

type ApiResponse = InferPattern<typeof ApiResponse>;

function handleResponse(raw: unknown) {
  const result = pattern.is.validated(raw, ApiResponse);
  if (!result.ok) {
    throw new Error(`Invalid response at ${result.path.join(".")}: ${result.reason.join(", ")}`);
  }
  return result.value; // fully typed ApiResponse
}