<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/pkg-types-vs-read-pkg-vs-read-package-up-reading-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/pkg-types-vs-read-pkg-vs-read-package-up-reading-2026/raw.md -->
<!-- Source path: content/guides/pkg-types-vs-read-pkg-vs-read-package-up-reading-2026.mdx -->

---
og_image: "/images/guides/pkg-types-vs-read-pkg-vs-read-package-up-reading-2026.webp"
title: "pkg-types vs read-pkg vs read-package-up 2026"
description: "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."
date: "2026-03-09"
author: "PkgPulse Team"
tags: ["nodejs", "typescript", "developer-tools", "automation"]
---

## 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](https://github.com/unjs/pkg-types) — typed package.json utilities:

### Read package.json

```typescript
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

```typescript
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

```typescript
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](https://github.com/sindresorhus/read-pkg) — read and normalize package.json:

### Basic usage

```typescript
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

```typescript
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

```typescript
import { readPackageSync } from "read-pkg"

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

---

## read-package-up

[read-package-up](https://github.com/sindresorhus/read-package-up) — find nearest package.json:

### Basic usage

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

| Feature | pkg-types | read-pkg | read-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 | ❌ | ✅ | ✅ |
| Dependencies | 0 | Few | read-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

| Package | Min+gzip | Find + read time | Sync 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:**

```typescript
// 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:**

```typescript
// 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:

| Need | Best Choice |
|------|-------------|
| Read current project's package.json with full TypeScript types | pkg-types |
| Read any package.json and normalize shorthand fields | read-pkg |
| Find the nearest package.json by walking up directories | read-package-up |
| Read tsconfig.json or find workspace root | pkg-types |
| Need a synchronous API | read-pkg or read-package-up |
| In the UnJS ecosystem | pkg-types |
| Building a CLI that runs from any project directory | read-package-up |
| Get both the parsed content and the file path | read-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.

```typescript
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.

```typescript
// 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 →](https://www.pkgpulse.com)*

*See also: [patch-package vs pnpm patch vs yarn patch](/guides/patch-package-vs-pnpm-patch-vs-yarn-patch-patching-node-2026) and [taze vs npm-check-updates vs npm-check](/guides/taze-vs-npm-check-updates-vs-npm-check-dependency-2026), [archiver vs adm-zip vs JSZip (2026)](/guides/archiver-vs-adm-zip-vs-jszip-zip-archive-creation-2026).*
