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

unused-props

v0.3.0

Published

Find React components whose declared props no caller passes

Readme

unused-props

Analyse a TypeScript+React codebase. Find every component prop, find every JSX usage, and report any prop that no caller passes.

How?

It parses your TypeScript source with ts-morph, enumerates each component's own declared props (skipping members inherited from extends clauses and from types declared outside your source directory), then scans every JSX usage to see which props are actually passed. Anything left over is reported.

Usage

unused-props ./src --tsconfig ./tsconfig.json
Scanned 48 components with declared own props.
Found 2 component(s) with at least one prop never passed by a caller (excluding ignored callers).

src/components/Button.tsx:12  Button
  4 caller(s)
    - variant  (optional)

src/components/Card.tsx:8  Card
  3 caller(s), 1 with {...spread}
    - elevation  (optional)
  Warning: 1 caller(s) use {...spread} — listed props may be passed through.

Full JSON report: unused-props.report.json

Pipe-friendly:

unused-props ./src --no-report --json-only | jq '.findings[] | .component'

CI use — fail the build on any finding, treat spread callers as errors:

unused-props ./src --error-on-spread

Install

pnpm add -D unused-props
# or
npm install --save-dev unused-props

Requires Node ≥ 20.

API

CLI

unused-props <src-dir> [options]

| Flag | Default | Notes | | ----------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | -t, --tsconfig <path> | tsconfig.json | Used to load the project's source set. | | -r, --report <path> | unused-props.report.json | Write the full JSON report to this path. | | --no-report | — | Skip writing the report. With --json-only, JSON is printed to stdout instead. | | --ignore-callers <glob...> | **/*.stories.{ts,tsx}, **/*.test.{ts,tsx}, **/*.spec.{ts,tsx} | picomatch globs for files whose JSX usages should NOT count as real callers. Matched against the path relative to <src-dir>. Pass your own patterns to override; pass an unsatisfiable glob (e.g. __never__) to count every caller. | | --json-only | false | Suppress human-readable stdout output. | | --error-on-spread | false | Exit with code 2 if any finding has callers using {...spread}. | | --error-on-unused-values | false | Exit non-zero if any value-bearing prop (literal union, boolean, enum) has unused declared values. By default these are informational. | | --error-on-redundant-defaults | false | Exit non-zero if any prop's destructuring default is never overridden by a real caller. By default these are informational. |

Exit codes: 0 clean · 1 findings (or --error-on-unused-values triggered) · 2 invalid args or --error-on-spread triggered.

Argument parsing is provided by commander.

Programmatic

import { scan, type ScanOptions, type ScanResult } from "unused-props";

declare function scan(opts: ScanOptions): ScanResult;

interface ScanOptions {
  projectRoot: string;            // file paths in findings are relative to this
  srcDir: string;                 // absolute path to your source root
  tsconfigPath: string;           // absolute path to the tsconfig used by ts-morph
  ignoreCallerPatterns: string[]; // picomatch globs; matched relative to srcDir
}

Example:

import { scan } from "unused-props";

const result = scan({
  projectRoot: process.cwd(),
  srcDir: "/abs/path/to/src",
  tsconfigPath: "/abs/path/to/tsconfig.json",
  ignoreCallerPatterns: ["**/*.stories.{ts,tsx}", "**/*.test.{ts,tsx}"],
});

for (const f of result.findings) {
  console.log(f.component, f.unusedProps.map((p) => p.name));
}

What counts as an "own" prop?

The tool reports only props you declared yourself. Members that flow in via type composition are treated as inherited and skipped:

  • Interface with extends — only direct members of the interface are reported. Anything from the parent (e.g. extends HTMLAttributes<...>) is skipped.
  • Type alias intersectiontype Foo = { ... } & Bar. Members of the inline { ... } are own; members of any referenced Bar are skipped.
  • Type references outside your <src-dir> — skipped entirely. This is why React.HTMLAttributes, MUI props, etc. don't appear in findings.

Move a prop into the child interface or the inline literal to monitor it.

Unused prop values

In addition to unused props, every run also reports unused values within value-bearing prop types: string/number literal unions, boolean, and TS enums. If priority: "primary" | "secondary" is always passed "primary", the "secondary" branch is reported — the type is narrower than its usage and the abstraction can be tightened.

null and undefined count as first-class candidate values when they appear in the union (1 | undefined | null | "example" reports any of the four that no caller passes). Optional props (prop?: T) implicitly include undefined as a candidate.

What counts as "passing" a value:

  • A literal JSX attribute: priority="secondary", count={2}, enabled, enabled={true}, value={null}.
  • A reference whose type resolves to a single literal: priority={Size.S} or priority={"x" as const}. Variables typed as the full union (prop: "a" | "b") are treated as dynamic — the finding is annotated, the listed values may actually be passed.
  • A destructuring default (({ priority = "primary" }) => …) — the default value counts as passed.
  • For optional props with no default, a caller that omits the prop contributes undefined to the "passed" set.

By default these findings are informational and don't change the exit code. Pass --error-on-unused-values to fail the run when any are present.

Redundant prop defaults

Separate from the value-coverage analysis above, the scanner also reports destructuring defaults that no real caller ever overrides. For a component like:

const Header = ({ prefix = "Hello, ", children }: HeaderProps) =>
  <h1>{prefix}{children}</h1>;

if every caller either omits prefix or passes prefix="Hello, " explicitly, the default is doing no real work — the prop could be removed entirely (or the default dropped).

The analysis only fires when at least one caller actually exercises the default (otherwise the prop would just be unused, which is the existing unused-props analysis). Findings are skipped if the prop is already reported as fully unused.

By default these findings are informational. Pass --error-on-redundant-defaults to fail the run when any are present.

Known limitations

  • Spread props ({...spread}) — when a caller spreads, we can't statically tell what's forwarded. Each finding carries both spreadCallers and nonSpreadCallers so you can triage: if any caller wrote attributes explicitly without the unused prop, that's strong evidence the prop is dead even if other callers spread. --error-on-spread fails the run when any finding has spread callers.
  • Dynamic JSXReact.createElement(...) and components rendered via a variable (const C = condition ? A : B; <C />) aren't traced.
  • Prop flow through helpers / HOCs — if a wrapper function receives props and forwards them to the real component, the wrapper isn't JSX so it doesn't count as a caller.
  • Generic components — type parameters may not resolve to a concrete set of props; coverage may be partial.
  • Components without an explicit prop type annotation — skipped; we can't enumerate props from inference alone.
  • Components only used by ignored callers — skipped. Use knip or similar to find truly unused components.
  • Dynamic prop values — for the unused-values analysis, an expression whose type doesn't narrow to a single literal (e.g. prop={someVar} where someVar: "a" | "b") is treated as dynamic. The finding still lists "unused" values but is annotated with a warning that they may actually be passed at runtime.

Development

pnpm install
pnpm test       # vitest run
pnpm typecheck  # tsc --noEmit across src + tests
pnpm build      # compile to dist/

Fixtures live in tests/fixtures/, each a minimal TS+React project exercising one scenario.

Versioning

This repo uses Changesets to manage versions. Any change that affects published behaviour requires a changeset, declaring the bump level:

pnpm changeset

Pick (while on 0.x):

  • patch — bug fixes and internal-only refactors.
  • minor — additive features and breaking changes.
  • major — avoid. Changesets jumps any major change on a pre-1.0 package straight to 1.0.0. Pre-1.0 convention is to treat everything as potentially breaking; record breaking changes as minor until we intentionally cut 1.0.

Once at 1.0+, switch to standard semver: minor for additive, major for breaking.

Changesets accumulate in .changeset/ and are consumed at release time:

pnpm changeset version   # bumps package.json + writes CHANGELOG.md
pnpm changeset publish   # publishes to npm (runs prepublishOnly → build)

License

MIT