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

@byteslayer/ts-check

v0.3.1

Published

Simple runtime Typecheck generator for TypeScript, write your Types once and be safe everywhere.

Readme

ts-check

Simple runtime Typecheck generator for TypeScript — write your types once and be safe everywhere.

ts-check turns your existing TypeScript types into runtime checks. Annotate a function, and ts-check reads its real parameter and return types (via the actual TypeScript compiler, not a re-implementation) and generates if guards that throw a TypeCheckError when a call doesn't match — no schema to write, no validator to keep in sync by hand.

/** @Check */
function total(order: Order, pricePerUnit: number): number {
  return order.items.reduce((sum, i) => sum + i.quantity * pricePerUnit, 0);
}

total({ id: "o2", items: [{ sku: "x", quantity: "two" }] }, 5);
// TypeCheckError: total(): argument "order" expected Order, got Object

That check didn't exist in your source. ts-check generated it at build time from the Order type, and wove it into the compiled output.

Why

TypeScript types disappear at compile time. A function signature is a promise to the compiler, not a guarantee at runtime — a bad API response, JSON.parse, or a caller who ignored a type error will walk straight past order: Order and into your function body unchecked.

The usual fix is a parallel runtime schema (Zod, io-ts, ...) that you write by hand and keep in sync with the type yourself. ts-check skips the duplication: it derives the check from the type you already wrote, at build time, so it can't drift out of sync — there's nothing second to maintain.

How it plugs in

ts-check is a ts-patch plugin — it hooks into real tsc, rather than replacing it. Your existing build (tsc, tsc -b, --watch, CI scripts, whatever you already run) keeps working; you're adding a plugin entry to tsconfig.json, not swapping compilers. That gets you everything tsc already does — incremental builds, project references, composing with other tsc plugins — for free.

Install

npm install --save-dev @byteslayer/ts-check

ts-patch and typescript come along as direct dependencies — nothing else to install. That pulls in ts-patch 3.x, which targets TypeScript 5.x; for a TypeScript 6.x project, override ts-patch to ^4.0.0 (e.g. via npm's overrides) to match.

Usage

Add a plugins entry under compilerOptions pointing at @byteslayer/ts-check/transformer:

{
  "compilerOptions": {
    // ...your normal options...
    "plugins": [
      { "transform": "@byteslayer/ts-check/transformer", "mode": "explicit", "include": ["*"] }
    ]
  }
}

Then build with tspc — ts-patch's drop-in tsc, no persistent patching needed — instead of plain tsc:

tspc -p tsconfig.json

Plain unpatched tsc silently ignores the plugins entry and produces unchecked output — you must build with tspc, or run ts-patch install once to persistently patch your project's typescript install so plain tsc/tsc -b picks the plugin up too (see ts-patch's own docs for that route).

| Key | Values | Meaning | |---|---|---| | mode | "explicit" (default) | "implicit" | explicit: only functions tagged /** @Check */ get checks. implicit: every checkable function in an included file gets checks, no tag needed. | | include | glob array, default ["*"] | Which files (relative to the tsconfig's directory) ts-check instruments. Supports *, **, ? — e.g. ["src/**/*.ts"]. Files outside this list compile normally, untouched. | | exclude | glob array, default [] | Carves files back out of include, same glob syntax — the reverse of include. A file must match include and not match exclude to be instrumented. | | use | object, see below | Per-feature on/off switches. Every feature defaults to true; list only the ones you want to turn off. | | options.generateChecksForInferredTypes | boolean, default true | When false, only parameters/return types with an explicit type annotation are checked — inferred ones are skipped. |

Turning off individual features (use)

use is a single object with one boolean key per kind of check ts-check can generate. Every key defaults to true — you only need to list the ones you want disabled:

{
  "transform": "@byteslayer/ts-check/transformer",
  "mode": "implicit",
  "use": {
    "dates": false,        // stop checking Date instances
    "generics": false,     // skip checking a generic against its constraint
    "yieldChecks": false   // stop checking generator `yield` values
  }
}

| Key | Governs | |---|---| | string, number, boolean, bigint, symbol, null, undefined, object, never | The primitive/keyword types. | | stringLiterals, numberLiterals, booleanLiterals, bigintLiterals, templateLiterals | Literal and template-literal types ("a", 42, `id-${number}`). | | arrays, tuples | T[]/Array<T> and fixed/variadic tuples, independently. | | objects, indexSignatures | Structural property checks on object/interface types, and { [key: string]: V } / { [key: number]: V } index signatures. | | unions, intersections | A \| B and A & B. | | classes | instanceof checks for a class declared in, or safely referenceable from, the file. | | dates, regExps, collections, promises, errors | The known global instance types — Date; RegExp; Map/Set/WeakMap/WeakSet; Promise; Error. | | functionTypes | Whether a function-typed value is checked for being a function at all. | | generics | Checking an unresolved type parameter against its extends constraint. | | parameterChecks | Parameter guards at the top of a checkable function's body. | | returnValueChecks | return <expr>; checks in an ordinary (non-async, non-generator) function. | | asyncFunctionReturnChecks | return <expr>; in an async function, checked against the awaited type. | | generatorReturnChecks | return <expr>; in a generator, checked against its TReturn. | | yieldChecks | yield <expr>; in a (non-async) generator. | | asyncGeneratorYieldChecks | yield <expr>; in an async function*, checked against the awaited type. | | redundantParamGuardElimination | Dropping a non-exported function's parameter guard when every call to it in the file provably already supplies an already-validated value. See "Redundant check elimination" below. |

Disabling a feature never falls back to a stricter check — it degrades to "no check" (the same permissive treatment any/unknown already get), so a disabled feature can only make ts-check quieter, never break a call that worked before. classes is the one exception: turning it off only disables the instanceof optimization — a class instance that isn't a safe instanceof target already falls back to a structural check (see COVERAGE.md), and classes: false falls back the same way instead of skipping the value entirely. redundantParamGuardElimination inverts this: it's purely an optimization, so turning it off is what makes output more verbose — it never changes which bad calls get caught either way.

Redundant check elimination

ts-check drops a parameter guard when it can prove, from the rest of the file, that the check is redundant:

function process(order: Order) {
  return charge(order); // order already validated by process's own guard
}

function charge(order: Order) { /* ... */ } // charge's guard on `order` is dropped

charge isn't exported and is only ever called with order — the exact value process's own guard already validated — so charge's guard would just recheck something already known to be true. ts-check builds a dependency graph across every checkable function in the file to work this out, and only elides a guard when it's certain: the callee must be a non-exported function whose address is never taken (only ever called directly), every call site must pass the same identifier, unmodified, from a parameter that's itself already checked with the exact same type, and a cycle of functions trusting only each other — with no real check backing any of them — keeps every guard in it rather than eliminating based on nothing. Any doubt keeps the guard; this never changes which bad calls get caught, only how many times an already-proven-valid value gets re-validated. Turn it off (see the use table above) if you'd rather see every guard generated as-is.

No plugins entry (or none naming @byteslayer/ts-check/transformer) means ts-check does nothing — the file compiles exactly as plain tsc would, zero overhead.

Marking what to check

In explicit mode (the default), tag a function with the @Check JSDoc comment — it's plain, always-valid TypeScript syntax, no import required:

/** @Check */
function greet(name: string) { ... }

/** @Check */
const double = (n: number) => n * 2;

class OrderService {
  /** @Check */
  place(order: Order) { ... }

  // no tag -> stays unchecked
  cancel(id: string) { ... }
}

In implicit mode every function, method, constructor, and accessor in an included file is checked automatically — no tags needed.

Running the checked code

ts-check's build produces ordinary JS. A failed check throws a TypeError (named TypeCheckError) with a message naming the function, the offending parameter (or "return value"), the expected type, and a short description of what was actually received:

TypeCheckError: total(): argument "order" expected Order, got Object
TypeCheckError: total(): return value expected number, got string

Framework integrations

The setup above works for a plain tsc/tspc build. Bundler-based frameworks don't run one: Vite (and everything built on it — Nuxt, SvelteKit, Astro) transforms .ts/.tsx with esbuild during dev and build, which only strips types and never runs tsc's transformer plugins; Next.js's default pipeline (SWC) does the same. Either way, compilerOptions.plugins in your tsconfig is silently ignored for the actual app bundle — you'd only see checks from a separate, standalone type-check step. These integrations route the matched files through a real ts.Program emit (via ts-patch's patched compiler) first, so the transformer actually runs.

Plain tsc backends (Express, NestJS, ...) — no integration needed

If your build is already just tsc/ts-patch-driven — a plain Node/Express backend, or NestJS's default tsc builder ("builder": "tsc" in nest-cli.json, the default) — you're already covered by Install and Usage above; nothing framework-specific to add. If you want plain tsc/tsc -b/watch mode to pick the plugin up too, not just tspc, run ts-patch install once (see Usage) to persistently patch your project's typescript install. NestJS's "builder": "swc" option isn't covered — same reason as esbuild/SWC everywhere else: it never runs tsc's transformer plugins.

Nuxt, SvelteKit, Astro, React, Vue, Preact, Solid, Remix, React Router (Vite)

// nuxt.config.ts
import { nuxtTsCheckPlugin } from "@byteslayer/ts-check/vite";

export default defineNuxtConfig({
  vite: { plugins: [nuxtTsCheckPlugin(import.meta.dirname)] },
});
// svelte.config.js / vite.config.ts
import { svelteKitTsCheckPlugin } from "@byteslayer/ts-check/vite";
plugins: [svelteKitTsCheckPlugin(import.meta.dirname)];
// astro.config.mjs
import { astroTsCheckPlugin } from "@byteslayer/ts-check/vite";
export default defineConfig({
  vite: { plugins: [astroTsCheckPlugin(new URL(".", import.meta.url).pathname)] },
});

For a plain Vite-based app — React, Vue, Preact, Solid, React Router v7 (framework mode), or Remix in Vite mode (remix vite:build, the current default) — the same generic plugin applies, exported under a per-framework name purely for discoverability (reactTsCheckPlugin, vueTsCheckPlugin, preactTsCheckPlugin, solidTsCheckPlugin, reactRouterTsCheckPlugin, remixTsCheckPlugin — all identical, all default to your project's own tsconfig.json):

// vite.config.ts
import { defineConfig } from "vite";
import { reactTsCheckPlugin } from "@byteslayer/ts-check/vite";

export default defineConfig({
  plugins: [reactTsCheckPlugin(import.meta.dirname)],
});

Classic Remix's pre-Vite esbuild compiler isn't covered — same story as Next.js's Turbopack below.

Each preset just points the generic tsCheckVitePlugin({ rootDir, tsconfigPath }) at the tsconfig you'd actually add a plugins entry to (Nuxt: .nuxt/tsconfig.app.json, the one Nuxt's own typescript.tsConfig merges into; SvelteKit and Astro: your project's own root tsconfig.json by default — SvelteKit's generated .svelte-kit/tsconfig.json is overwritten by svelte-kit sync and isn't meant to be edited directly, so point at the root one it extends, same as you already do for your own compiler options). Pass a different tsconfigPath if your setup differs. Only plain .ts/.tsx files are covered; .vue/.svelte/.astro component <script> blocks go through their own framework-specific pipeline.

Next.js (webpack)

// next.config.js
const { withTsCheck } = require("@byteslayer/ts-check/webpack");

module.exports = withTsCheck({
  // ...your existing config
});

Requires webpack mode (next build / next dev, no --turbo) — Turbopack doesn't support custom webpack loaders. Pass { tsconfigPath: "..." } as a second argument if your tsconfig isn't at the project root.

Not supported: Angular, Ember

Verified against a real ng build and deliberately not shipped: Angular's esbuild-based CLI build doesn't route through the ts-patch-patched typescript package the way plain tsc and Vite's esbuild-fallback do — @angular/compiler-cli's own program wrapper (NgtscProgram) sidesteps it, so ts-patch install alone silently does nothing, and there's no equivalent of a Vite transform hook or webpack loader to fall back to without integrating against Angular's own compiler internals. Ember's build (Broccoli) has no ts.Program-based hook at all — closer to Angular's problem than to Vite's. Both would need dedicated, framework-specific work this package doesn't do today; support isn't claimed for either.

What gets checked

Parameters and return values of any checkable function — function declarations/expressions, arrow functions, class methods, constructors, and get/set accessors. ts-check works from the TypeScript compiler's own resolved types, so it covers primitives, literals, template literal types, arrays/tuples, objects, interfaces, unions/intersections (including discriminated unions), enums, classes (via instanceof), recursive/self-referential types, generics instantiated with concrete types, and the standard utility types (Partial, Pick, Omit, Record, ...).

async function return values are checked against the awaited (resolved) type, not the declared Promise<T> — a failed check surfaces as a rejected promise rather than a synchronous throw, same as any other async error. Generator functions get the same treatment for yield <expr>; and return <expr>;, when used as their own statements — including async function* generators, whose yielded values the JS runtime itself awaits before a caller sees them, checked the same resolved-value way as an async function's return.

It deliberately skips what can't be soundly checked: generic type parameters that are erased by the time the code runs, void/any/unknown, an async generator's return value specifically (unlike its yield, the runtime doesn't await it, and TypeScript's own types don't guarantee its shape either — no sound value to check against), yield used as a sub-expression or yield* delegation, and a function value's own signature (only that it is a function).

See COVERAGE.md for the full feature-by-feature matrix — every row backed by a passing test.

Examples

examples/ has runnable, self-contained projects, each with its own package.json/tsconfig.json:

| Example | Demonstrates | |---|---| | examples/demo | A tour of common types, generics, and a small "order total" scenario with a checked function catching a bad call | | examples/explicit | mode: "explicit" — only @Check-tagged functions are instrumented | | examples/implicit | mode: "implicit" — every function is instrumented automatically. Includes feature-tour.ts (a sweep across nearly every checkable TS/JS construct: primitives, literals, template literal types, tuples, objects, symbol-keyed properties, unions, enums, classes, generics, utility types, known globals, every function form) and async-tour.ts (async functions, generators, async generators) — both call every generated check with a good and a bad value | | examples/coverage | A denser sampler across more of the type-system surface in COVERAGE.md |

Run one, from the repo root:

bun run build   # compiles src/tsPatchPlugin.ts to dist/, for Node to require

cd examples/demo
bun install      # picks up "@byteslayer/ts-check": "file:../.." from package.json
bun run build     # tspc -p tsconfig.json
bun run start      # runs the compiled output

How it works

  1. src/analyze.ts walks the program before emit, using the real ts.TypeChecker to resolve every checkable parameter/return type and build the corresponding validator expressions (src/typeToValidator.ts). This has to happen in its own pass, before emit — calling the checker from inside a transformer during emit crashes TypeScript internally.
  2. src/transformer.ts is a pure structural transform: given the precomputed analysis, it splices parameter guards in at the top of each function body and rewrites every return to check its value first, with zero further checker calls.
  3. src/runtimePrelude.ts supplies the small __typeChecks runtime helper (primitive predicates, error formatting) injected once per file that needs it.
  4. src/tsPatchPlugin.ts is the plugin entry point ts-patch calls: (program, pluginConfig) => TransformerFactory, invoked once per compilation with the Program ts-patch already built. No reimplemented build driver — real tsc does everything else (parsing, diagnostics, incremental caching, emit).

@Check-tag detection (analyze.ts's nodeHasCheckTag) scans leading comment text directly, rather than going through ts.getJSDocTags/ node.jsDoc. ts-patch's live compiler (tspc) parses source with JSDoc-to-AST association skipped for performance (JSDocParsingMode.ParseNone), which leaves the JSDoc AST empty even though the comment is right there in the source — scanning text sidesteps that entirely.

Development

bun install
bun test      # tests across src/analyze, transformer, glob, and generated-code behavior
bun run build  # tsc -p tsconfig.build.json (compiles src/tsPatchPlugin.ts + deps to dist/)

Tests run the real pipeline (analyze → transform → emit) in memory via test/harness.ts, then assert on both the generated code and its actual runtime behavior — not just that some code was emitted.

Limitations

  • Structural, not exact. Like TypeScript itself, ts-check checks that required shape is present — it doesn't reject excess properties.
  • readonly is unenforceable at runtime — same as in TypeScript, compile-time only.
  • A function-typed value is only checked for being a function — its own parameter/return signature can't be verified without calling it.
  • Generic type parameters are erased. Anything that depends on an unconstrained <T> can't be checked; the parts of the type that don't depend on T still are.
  • void/any/unknown are intentionally not checked — see COVERAGE.md for why.
  • A failed async function check (or an async function*'s yield check) rejects the returned promise instead of throwing synchronously — the correct shape for an async error, but worth knowing if you're used to every other ts-check failure throwing immediately.
  • An async function*'s return value is never checked — its yields are, but the runtime doesn't await the returned value the way it awaits a yielded one, and TypeScript's own types don't guarantee its shape either; see COVERAGE.md for the full reasoning.
  • yield used as a sub-expression and yield* delegation are not checked — see COVERAGE.md for why.
  • Template literal type numbers use decimal notation only — a `${number}` placeholder won't match "NaN", "Infinity", or exponential notation, even though those are valid runtime values of type number.
  • Depends on TypeScript's internal transformer API, which is not guaranteed stable across major TS versions, and on ts-patch's own compatibility with your TypeScript version — ts-check supports both TS5 and TS6, via ts-patch 3.x (TypeScript 5.x) and ts-patch 4.x (TypeScript 6.x) respectively; match the major version to what your project uses.

License

MIT