Skip to main content

Guide

pkg-types vs read-pkg vs read-package-up 2026

Compare pkg-types, read-pkg, and read-package-up for reading and parsing package.json files in Node.js. Type-safe package info, monorepo traversal now.

·PkgPulse Team·
0

TL;DR

pkg-types is the UnJS package for reading package.json — TypeScript types for package.json fields, finds the nearest package.json, resolves workspaces, and exports typed utilities. read-pkg reads and normalizes a single package.json — parses, validates, normalizes fields (like man, bin). read-package-up (formerly read-pkg-up) walks up directories to find the nearest package.json — useful for tools that need to find the project root. In 2026: pkg-types for typed package.json access, read-package-up for traversal, read-pkg for normalized parsing.

Key Takeaways

  • pkg-types: ~10M weekly downloads — UnJS, TypeScript types for package.json, workspace resolution
  • read-pkg: ~15M weekly downloads — reads + normalizes package.json, Sindre Sorhus
  • read-package-up: ~15M weekly downloads — finds nearest package.json by walking up directories
  • pkg-types provides full TypeScript types for every package.json field
  • read-pkg normalizes package.json (e.g., converts bin string to object format)
  • All three are commonly used by CLI tools, bundlers, and linters

pkg-types

pkg-types — typed package.json utilities:

Read package.json

import { readPackageJSON, findWorkspaceDir, resolvePackageJSON } from "pkg-types"

// Read the nearest package.json:
const pkg = await readPackageJSON()
// → { name: "my-app", version: "1.0.0", dependencies: {...}, ... }

// Read from a specific path:
const pkg2 = await readPackageJSON("/path/to/project")

// Find the nearest package.json path:
const pkgPath = await resolvePackageJSON()
// → "/Users/royce/project/package.json"

// Find workspace root:
const workspaceRoot = await findWorkspaceDir()
// → "/Users/royce/monorepo" (root with workspaces config)

TypeScript types

import type { PackageJson } from "pkg-types"

// Full TypeScript types for package.json fields:
const pkg: PackageJson = {
  name: "pkgpulse",
  version: "1.0.0",
  type: "module",
  main: "./dist/index.cjs",
  module: "./dist/index.js",
  types: "./dist/index.d.ts",
  exports: {
    ".": {
      import: { types: "./dist/index.d.ts", default: "./dist/index.js" },
      require: { types: "./dist/index.d.cts", default: "./dist/index.cjs" },
    },
  },
  dependencies: { "react": "^19.0.0" },
  devDependencies: { "typescript": "^5.5.0" },
  peerDependencies: { "react": ">=18" },
  scripts: { build: "tsup", test: "vitest" },
}

// Type-safe access:
pkg.name        // string | undefined
pkg.version     // string | undefined
pkg.exports     // Exports | undefined (full conditional exports type)
pkg.type        // "module" | "commonjs" | undefined

TSConfig utilities

import { readTSConfig, resolveTSConfig } from "pkg-types"

// Read tsconfig.json:
const tsconfig = await readTSConfig()
// → { compilerOptions: { target: "ES2022", ... }, include: [...] }

// Find the nearest tsconfig.json:
const tsconfigPath = await resolveTSConfig()
// → "/Users/royce/project/tsconfig.json"

read-pkg

read-pkg — read and normalize package.json:

Basic usage

import { readPackage } from "read-pkg"

// Read from current directory:
const pkg = await readPackage()
// → { name: "my-app", version: "1.0.0", ... }

// Read from specific directory:
const pkg2 = await readPackage({ cwd: "/path/to/project" })

// Normalize (converts shorthand fields to full format):
const pkg3 = await readPackage({ normalize: true })

Normalization

import { readPackage } from "read-pkg"

// Before normalization:
// package.json: { "bin": "./cli.js", "man": "./man/doc.1" }

const pkg = await readPackage({ normalize: true })

// After normalization:
// pkg.bin → { "my-app": "./cli.js" }  (string → object)
// pkg.man → ["./man/doc.1"]           (string → array)
// pkg.repository → { type: "git", url: "..." } (string → object)

Sync version

import { readPackageSync } from "read-pkg"

// Synchronous read:
const pkg = readPackageSync()
const name = pkg.name
const version = pkg.version

read-package-up

read-package-up — find nearest package.json:

Basic usage

import { readPackageUp } from "read-package-up"

// Walks up from cwd to find nearest package.json:
const result = await readPackageUp()

if (result) {
  console.log(result.packageJson.name)  // Package name
  console.log(result.path)               // Full path to package.json
  // → "/Users/royce/project/package.json"
}

// From a specific directory:
const result2 = await readPackageUp({
  cwd: "/Users/royce/project/src/utils/deep/nested",
})
// Walks up: nested → deep → utils → src → project (found!)

Use case: Finding project root

import { readPackageUp } from "read-package-up"
import path from "node:path"

async function findProjectRoot(startDir?: string) {
  const result = await readPackageUp({ cwd: startDir })
  if (!result) throw new Error("No package.json found")
  return path.dirname(result.path)
}

// Usage in a CLI tool:
const projectRoot = await findProjectRoot()
console.log(`Project root: ${projectRoot}`)

Use case: Getting package info from anywhere

import { readPackageUp } from "read-package-up"

// In a deeply nested file, find the package it belongs to:
async function getPackageInfo() {
  const result = await readPackageUp()
  if (!result) return null

  return {
    name: result.packageJson.name,
    version: result.packageJson.version,
    root: path.dirname(result.path),
  }
}

// Useful for:
// - CLI tools that need the project name
// - Bundlers that need package.json metadata
// - Linters that need to know project boundaries

Common Patterns

Version detection

import { readPackageJSON } from "pkg-types"

// Check if a dependency is installed and get its version:
async function getDependencyVersion(name: string): Promise<string | null> {
  try {
    const pkg = await readPackageJSON(`node_modules/${name}`)
    return pkg.version ?? null
  } catch {
    return null
  }
}

const reactVersion = await getDependencyVersion("react")
// → "19.0.0" or null

Workspace detection

import { readPackageJSON, findWorkspaceDir } from "pkg-types"

async function getWorkspacePackages() {
  const rootDir = await findWorkspaceDir()
  if (!rootDir) return []

  const rootPkg = await readPackageJSON(rootDir)
  const workspaces = rootPkg.workspaces

  if (!workspaces) return []

  // workspaces could be string[] or { packages: string[] }
  const patterns = Array.isArray(workspaces)
    ? workspaces
    : workspaces.packages ?? []

  return patterns
}

Feature Comparison

Featurepkg-typesread-pkgread-package-up
Read package.json✅ (with traversal)
Walk up directories✅ (resolvePackageJSON)
TypeScript types✅ (full)✅ (basic)✅ (via read-pkg)
Normalization
TSConfig reading
Workspace detection
Sync API
Dependencies0Fewread-pkg
UnJS ecosystem
Weekly downloads~10M~15M~15M

When to Use Each

Use pkg-types if:

  • Need full TypeScript types for package.json fields
  • Want workspace and tsconfig.json utilities
  • In the UnJS ecosystem
  • Building tools that need typed package metadata

Use read-pkg if:

  • Need normalized package.json (shorthand fields expanded)
  • Want a synchronous API option
  • Building tools that parse and validate package.json

Use read-package-up if:

  • Need to find the nearest package.json from any directory
  • Building a CLI tool that runs from anywhere in the project
  • Need both the parsed content and the file path

Real-World Usage: Who Uses What in 2026

All three packages appear as transitive dependencies in the Node.js ecosystem, but for different reasons:

pkg-types is a dependency of unbuild, Nitro, and the wider UnJS ecosystem. When you install Nuxt 3, you get pkg-types via the Nuxt build pipeline. Tools that need to read package.json in a cross-platform, type-safe way — particularly Nuxt's module system and Nitro's server config — use pkg-types because it provides TypeScript types for every field without requiring @types/ packages or manual type declarations.

read-pkg is pulled in by ESLint, Prettier, and other Sindre Sorhus packages. Sindre's ecosystem consistently uses read-pkg because it normalizes edge cases (like the bin field being a string vs an object) that matter for CLI tools published to npm. If you use ESLint or prettier in a project, read-pkg is almost certainly in your node_modules.

read-package-up is a dependency of tools that need to "find the project root" — linters, code generators, and migration scripts. ESLint uses it to find which package's configuration applies. Prettier uses it to determine the project root when searching for configuration files. It is the standard choice for any tool that runs from within a project directory and needs to locate the enclosing package.

Bundle Size and Performance

PackageMin+gzipFind + read timeSync API
pkg-types~3 KB~2-5ms
read-pkg~4 KB~3-6ms
read-package-up~5 KB~5-15ms (traversal)

read-package-up is slower because it traverses the directory tree — starting from the current working directory and walking up until it finds a package.json. For deeply nested projects, this means multiple filesystem stat calls. In practice this is not a bottleneck (milliseconds), but for tools that call read-package-up in a hot loop (like linting every file), caching the result is worth doing.

Migration Guide

From read-pkg to pkg-types:

// Before:
import { readPackage } from "read-pkg"
const pkg = await readPackage({ cwd: "/path/to/project" })
console.log(pkg.name, pkg.version)

// After:
import { readPackageJSON } from "pkg-types"
const pkg = await readPackageJSON("/path/to/project")
console.log(pkg.name, pkg.version)

// Key differences:
// 1. read-pkg normalizes fields (bin: string → object), pkg-types does not
// 2. pkg-types returns raw package.json structure with full TypeScript types
// 3. pkg-types has no sync equivalent — everything is async

From manual JSON.parse to any of these:

// Before (common pattern in many older tools):
import { readFileSync } from "node:fs"
import { join } from "node:path"
const pkg = JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8"))

// After with pkg-types (typed, finds nearest automatically):
import { readPackageJSON } from "pkg-types"
const pkg = await readPackageJSON()
// pkg.name is string | undefined — TypeScript knows the type

// After with read-package-up (traversal, includes path):
import { readPackageUp } from "read-package-up"
const result = await readPackageUp()
if (result) {
  const { packageJson, path } = result
  // Know both the content and where it came from
}

Decision Guide

Choosing between these three depends on what your tool needs to do with package.json:

NeedBest Choice
Read current project's package.json with full TypeScript typespkg-types
Read any package.json and normalize shorthand fieldsread-pkg
Find the nearest package.json by walking up directoriesread-package-up
Read tsconfig.json or find workspace rootpkg-types
Need a synchronous APIread-pkg or read-package-up
In the UnJS ecosystempkg-types
Building a CLI that runs from any project directoryread-package-up
Get both the parsed content and the file pathread-package-up

In practice, most build tools and CLIs want read-package-up — the combination of directory traversal plus the file path (which gives you the project root) covers the broadest set of tool-building scenarios. pkg-types is the better choice when you want rich TypeScript types and workspace utilities without the traversal overhead.

Security Considerations When Reading package.json

Reading package.json files from arbitrary paths introduces a class of security concern that is easy to overlook in tool development: path traversal. A CLI tool that reads package.json based on user-supplied paths should validate that the resolved path stays within the project boundaries before passing it to any of these libraries. read-package-up's traversal mechanism stops at the filesystem root, so it will never traverse outside the repository, but readPackageJSON(userSuppliedPath) from pkg-types does not apply any boundary checking — a malicious --cwd argument of ../../../../etc/ could cause unintended behavior if the calling code doesn't validate the path first.

The scripts field in package.json is a particular concern for tools that read and display or execute package metadata. A compromised or malicious package.json could contain scripts with shell injection payloads that become dangerous if your tool executes them. pkg-types, read-pkg, and read-package-up all return the raw scripts object without sanitization — tools that display script content to users should treat it as untrusted user-provided strings and escape output appropriately. In CI environments where these tools process package.json files from pull request branches (which may contain attacker-controlled content), the tool's behavior with adversarial scripts fields is worth auditing.

Ecosystem Compatibility and ESM Migration

All three packages are ESM-only in their current major versions (pkg-types v1.x, read-pkg v9.x, read-package-up v11.x), following Sindre Sorhus's ESM-first migration across his package ecosystem. This means they cannot be require()'d in CommonJS modules without dynamic import(). For tools that must support both ESM and CJS consumers, the typical approach is to use dynamic import() with a top-level await in an async initialization function, or to use the jiti package to enable ESM imports in CJS contexts without a build step.

The UnJS ecosystem (which pkg-types is part of) takes an ESM-first approach but publishes CJS builds as well through unbuild's dual-format output. pkg-types specifically ships both ESM and CJS builds, making it compatible with CommonJS tools that use dynamic import(). For CLI tools that need to work immediately without a build step, jiti combined with pkg-types is a common pattern — jiti handles the ESM-CJS boundary at runtime while pkg-types provides the typed package.json access. Teams building tools that must run in both contexts should test their require() and import() codepaths explicitly, as dual-format packages occasionally have subtle behavioral differences.

Handling Non-Standard package.json Extensions

Many package.json files in the wild contain non-standard extensions — fields like ava, jest, prettier, eslint configuration embedded directly in package.json rather than in separate config files. pkg-types's PackageJson type includes an index signature that permits arbitrary additional fields with type unknown, so accessing these non-standard fields requires type assertions. read-pkg normalizes the standard fields but passes through non-standard fields unchanged. The practical implication for tool authors is that reading configuration from package.json — common for lint rules, test configuration, or bundler settings — works transparently with all three libraries, but the TypeScript type for the non-standard fields requires either a type assertion or a custom type that extends PackageJson with the additional fields your tool reads. This is a minor ergonomic consideration but worth planning for when building tools that read tool-specific configuration from package.json.

Performance and Caching in Monorepos

In large monorepos with dozens of packages, reading package.json files frequently can become a noticeable overhead — especially in linters and bundlers that process every file. All three packages perform synchronous or async filesystem reads, which means repeated calls in hot paths benefit significantly from caching.

import { readPackageJSON } from "pkg-types"

// Cache the result to avoid repeated disk reads:
const packageCache = new Map<string, Awaited<ReturnType<typeof readPackageJSON>>>()

async function getCachedPackage(dir: string) {
  if (!packageCache.has(dir)) {
    packageCache.set(dir, await readPackageJSON(dir))
  }
  return packageCache.get(dir)!
}

For read-package-up, the traversal cost compounds in deep directory trees. In a monorepo with packages/ui/src/components/Button/ as the working directory, read-package-up must stat 5+ directories before finding packages/ui/package.json. Tools like ESLint cache this result per-file rather than re-traversing on every lint run.

// ESLint-style caching for read-package-up:
import { readPackageUp } from "read-package-up"
import path from "node:path"

const rootCache = new Map<string, string>()

async function findPackageRoot(cwd: string): Promise<string> {
  // Cache by directory, not by file path
  const dir = path.dirname(cwd)
  if (!rootCache.has(dir)) {
    const result = await readPackageUp({ cwd: dir })
    if (!result) throw new Error(`No package.json found from ${dir}`)
    rootCache.set(dir, path.dirname(result.path))
  }
  return rootCache.get(dir)!
}

pkg-types' findWorkspaceDir() is specifically optimized for monorepos — it stops traversal when it finds a workspace root (a package.json with a workspaces field or a pnpm-workspace.yaml), making it faster than read-package-up in workspace-aware tooling.

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on pkg-types v1.x, read-pkg v9.x, and read-package-up v11.x.

Compare package utilities and developer tooling on PkgPulse →

See also: patch-package vs pnpm patch vs yarn patch and taze vs npm-check-updates vs npm-check, archiver vs adm-zip vs JSZip (2026).

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.