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

@venn-lang/dts

v0.6.0

Published

Reads the types an installed npm package publishes and returns them as Venn TypeSpecs.

Readme

@venn-lang/dts

Reads the types an installed npm package publishes and returns them as Venn TypeSpecs.

A Venn file can import from a package it installed: import { z } from "zod". For the checker to say anything at all about z, something has to work out what zod publishes. That is this package. It asks the TypeScript compiler rather than parsing .d.ts text, and hands back plain data that @venn-lang/core can check against without ever learning what npm is.

Node only: it loads the TypeScript compiler API.

Usage

import { readPackageTypes } from "@venn-lang/dts";

const types = readPackageTypes({
  package: "zod",
  from: "/project/target/package.json",
});

types.exports.z; // a TypeSpec
types.covered; // { total: 42, dynamic: 3 }

from is the file the specifier is resolved against, using the same rules TypeScript itself uses. The CLI passes the generated target/package.json, beside which the package manager wrote node_modules.

A package that declares this:

export declare function sum(a: number, b?: number): number;

comes back as this:

{
  kind: "fn",
  params: [{ kind: "prim", name: "number" }, { kind: "prim", name: "number" }],
  result: { kind: "prim", name: "number" },
  takes: 1,
}

takes is how many arguments the caller must actually pass. Venn checks arity exactly, so without it z.string() would read as the wrong number of arguments against a signature with one optional parameter.

API

| Export | Signature | What it does | | --- | --- | --- | | readPackageTypes | ({ package, from }) => PackageTypes | Resolves the package's declaration file, runs a program over it, and converts every module export. | | PackageTypes | interface | { package, exports: Record<string, TypeSpec>, covered: { total, dynamic } }. | | toSpec | (type: ts.Type, conv: Conversion) => TypeSpec | One TypeScript type as a TypeSpec. For callers that already hold a ts.TypeChecker. | | Conversion | interface | What one conversion carries: the checker, the current depth, and the shared state (types already read, types open, budget left). |

covered is counted, not claimed. "94% of exports typed" is a number that can be checked and driven up, and it tells a reader far more than "fully compatible", which would be false for any package built on conditional types.

A package that cannot be resolved, or that ships no declarations, is not an error. Plenty ship none. The answer is an empty result, and every imported name is dynamic, which is the truth about it.

How a TypeScript type is projected

Everything TypeScript can say lands on one of the shapes in @venn-lang/types or degrades to dynamic. It never fails.

| TypeScript | TypeSpec | | --- | --- | | string, number, boolean | prim string / number / bool | | void, undefined | prim void | | null | prim null | | any, unknown, never | dynamic | | "GET", 200 | literal | | T \| U | union, with the undefined and null branches folded away | | T[] | list | | [A, B] | list of A \| B, since the language has no fixed-length list | | A callable | fn, from the first signature only | | An object with properties | record, open | | Anything else | dynamic |

string | undefined is one type in Venn, a string that may not be there, so it arrives as prim string. Writing it as a two-branch union would make every optional field something the reader has to take apart before using it.

A method is a field whose type is a function, because that is what it is in Venn too. schema.parse is reached the same way schema.shape is.

Records are open. The package's own type is the authority on what it holds and this reading of it is not, so anything missed stays reachable instead of being refused.

Three limits keep a real package finishable. Conversion stops at depth 4, at a budget of 20,000 types, and at any type that leads back to itself. A package's types form a graph rather than a tree: zod's z holds hundreds of members and most of them lead back to the same handful of shapes, so reading each branch on its own re-expands the same types until the machine runs out of memory. Reading each type once and stopping at a budget is what makes this finish.

Why the compiler

A modern package's exported types are built out of generics, conditional types and mapped types, and none of those mean anything until something resolves them. Parsing the text gives back the machinery; asking the compiler gives back the answer. A declaration of Unwrap<Promise<string>> arrives here as prim string.

TypeScript is pulled in under the alias tsc-api, pinned to the 5.x line. TypeScript 7 is Go-native and ships no JavaScript API until 7.1, so its package exports a version string and nothing else. 7 builds this repository, and 5.9 is a library this one package calls.

Where it is used

venn add and venn install derive the types once, after the package manager has run, and write them to target/types/<package>.json. venn check reads them back and binds them to the names a file imported, so a wrong argument to z.object is caught without running anything.

Deriving is done at install rather than on every check because reading a large package through the compiler takes about a second, and the answer only changes when what is installed does. The CLI also loads this package with a dynamic import(), so venn run never pays for ten megabytes of compiler it will not use.

See also

  • @venn-lang/types defines TypeSpec, the vocabulary everything here is converted into.
  • @venn-lang/core binds these specs to imported names and checks the calls against them.
  • @venn-lang/cli runs the derivation at install time and reads the result at check time.