Skip to content

Handling Issues

A long report can be frustrating. The list may contain false positives, but also tons of useful information. To get the most value out of Knip, it may require some initial configuration.

This guide helps you deal with false positives to find solutions and create the perfect report, with minimal configuration that will keep your project tidy.

If you start out using Knip in a large project and have a long report, it makes sense to go over the issue types one by one. For instance, reducing the number of unused files will also reduce the number of unused dependencies.

  1. Unused files
  2. Unused dependencies
  3. Unresolved imports
  4. Unused exports

Getting the list of unused files right trickles down into the other issue types as well, so we start here. Files are reported as unused if they are in the set of project files, but not in the set of files resolved from the entry files:

unused files = project files - (entry files + resolved files)

Let’s go over common causes for unused files:

In most cases you can add entry patterns manually.

Use --files to filter the report and focus only on unused files:

Terminal window
knip --files

This works with other issue types as well. For instance, use --dependencies to focus only on dependencies and exclude issues related to unused files and exports.

For certain features, Knip needs to run after relevant files are generated. For instance, source mapping in a monorepo may require files to be built into dist folders first. And generated files in the src directory may import other files. For instance, the src/routeTree.gen.ts file generated by @tanstack/router must exists so Knip can find the imported route files.

Solution: compile and/or generate the relevant files first so Knip can resolve and find the source files.

If Knip still reports false positives, you may need to strategically add an entry file manually.

Dynamic import specifiers aren’t resolved, such as:

const entry = await import(path.join(baseDir, 'entry.ts'));

Solution: add entry.ts to entry patterns.

Some tooling command arguments aren’t recognized:

{
"name": "my-lib",
"version": "1.0.0",
"scripts": {
"build": "unknown-build-cli --entry production.ts"
}
}

Solution: add production.ts to entry patterns.

This works the same for any script, also those in GitHub Actions workflows or Git hooks. See script parser for more details about Knip’s script parser.

Entry files referenced in HTML files (e.g. <script src="production.js">).

<html>
<body>
<script type="module" src="production.js"></script>
</body>
</html>

Solution: add production.js to entry patterns. Or add an .html compiler to extract and resolve the value of <script src> elements.

Knip has support for some popular framework formats through compilers, and additional compilers can be added for for any file type. The recommended solution is usually to add the file as shown in each example as an entry file.

You might be using a tool or framework for which Knip doesn’t have a plugin (yet). Configuration and entry files (and related dependencies) may be reported as unused because there is no plugin yet that would include those files. Two examples:

  • Configuration file tool.config.js contains a reference to the package "@tool/plugin" → both the file and the dependency are reported as unused.
  • A framework automatically imports all files matching src/models/*.ts → those files are reported as unused.

Solution: create a new plugin for the tool or framework that’s not in the list yet. Or work around it and add entry patterns and maybe ignore a dependency or two (using ignoreDependencies).

Files may be reported as unused if existing plugins do not include that entry file pattern yet. See the plugins section of entry files for more details.

Solution: override plugin configuration to customize default patterns for existing plugins. Or even better: send a pull request to improve the plugin.

When using TypeScript path aliases in import specifiers, aliases referencing other workspaces are not special-cased. This may cause false positives. For Knip, it’s better to be explicit and list other workspaces as dependencies in package.json.

Solution: move such “workspace aliases” from compilerOptions

tsconfig.json
{
"compilerOptions": {
"paths": {
"@org/common/*": ["packages/common/*"]
}
}
}

…to dependencies or devDependencies in package.json:

package.json
{
"name": "@org/lib",
"dependencies": {
"@org/common": "workspace:*"
}
}

An additional benefit is that Knip will report unused and unlisted dependencies from now on.

Also see FAQ: Why can’t I use path aliases to reference other workspaces?

Multiple instances of configuration files like .eslintrc and jest.config.json across the repository may be reported as unused when working in a (mono)repo with a single package.json.

Solution: see integrated monorepos for more details and how to configure plugins to target those configuration files.

Some frameworks have features like “auto-mocking” or “auto-imports” enabled, such as Jest and Nuxt.

Solution: include such entry files by extending the entry file patterns. This is recommended in most cases:

{
"entry": ["src/index.ts", "src/models/*.ts"]
}

Alternatively, exceptions and outliers can be excluded from the analysis using negated project patterns:

{
"project": ["src/**/*.ts", "!**/__mocks__/**"]
}

Dependencies imported in unused files are reported as unused dependencies. That’s why it’s strongly recommended to try and remedy unused files first. Better entry and project file coverage will solve many cases of reported unused dependencies.

The most common causes for unused dependencies include:

Use --dependencies to filter the report and focus only on issues related to dependencies:

Terminal window
knip --dependencies

If a plugin exists and the dependency is referenced in the configuration file, but its custom dependency finder does not detect it, then that’s a false positive. Please open a pull request or issue to fix it.

Solution: adding the configuration file as an entry file pattern may be a temporary stopgap that fixes your situation, but it’s better to create a new plugin or fix an existing one.

Sometimes a reference to a dependency is unrecognizable or unreachable to Knip, so it’s a false positive and incorrectly reported as unused.

Solution: add a new plugin or improve an existing one. If you don’t feel like a plugin could solve it, a last resort is to use ignoreDependencies.

If a binary (or “executable”) is referenced you’ll want to use ignoreBinaries instead. See unlisted binaries.

Many packages come with their type definitions bundled. This means the package.json#types field in the package points to an internal/local type definition file. In this case, the separate types package is obsolete.

Knip reporting this is also useful for future regressions: if a package had a DefinitelyTyped or similar package for its types before and later on starts shipping those types bundled with the source code, Knip will report the obsolete types dependency as unused.

Examples include ESLint, Webpack and React Router, rendering the @types/eslint, @types/webpack and @types/react-router dependencies obsolete respectively.

Solution: remove the types dependency (often @types/...)

Knip is strict in the divide between dependencies and devDependencies. Some packages are published with one or more type packages listed in dependencies. In strict production mode, even when re-exported and part of the package’s public API, Knip does not try to figure out what exactly are “production types” and expects those in devDependencies.

Solution: list exceptions in ignoreDependencies.

This means that a dependency is referenced directly in source code or configuration, but not listed in package.json.

An unlisted dependency is usually a transitive dependency that’s imported or referenced directly. The dependency is installed (since it’s a dependency of another dependency) and lives in node_modules, but it’s not listed explicitly in package.json.

You should not rely on transitive dependencies for various reasons, including control, security and stability.

Solution: install and list the dependency in dependencies or devDependencies.

Binaries are executable Node.js scripts. Some npm packages, when installed, add one or more executable files to use from scripts in package.json. Examples include TypeScript that comes with the tsc binary, ESLint comes with eslint, Next.js with next, and so on.

Knip detects such binaries in scripts and checks whether there’s a package installed that includes that binary. It looks up the bin field in the package.json file of installed packages. If it doesn’t find it, it will be reported as an unlisted binary as there is no package listed that contains it.

Binaries that are installed on the OS already and thus likely not meant to be installed from npm are not reported as unlisted (details: list of ignored binaries in source).

In case unused (dev) dependencies look like a match against unlisted binaries, then this might be caused by node_modules not containing the packages. And this might have been caused by either the way your package manager installs dependencies and binaries, or by not running Knip from the root of the repository.

Solution: run Knip from the project root. From there you can lint individual workspaces.

Sometimes binaries and how they’re reported can be a bit confusing. See this example:

{
"name": "lib",
"scripts": {
"commitlint": "commitlint --edit"
},
"devDependencies": {
"@commitlint/cli": "*"
}
}

This example works fine without anything reported, as the @commitlint/cli package includes the commitlint binary. However, some script may contain npx commitlint and here Knip assumes commitlint is the name of the package. This technically works, as commitlint is a transitive dependency of @commitlint/cli. However, correct and recommeded usage is by the package name: npx @commitlint/cli.

Knip may ignore or be unable to resolve an import specifier or dependency references. The most common causes for unresolved imports:

Using template strings in dynamic imports might be ignored or not handled properly by Knip, resulting in false positives. Examples of dynamic import template strings:

import(`./${value}.ts`);
import(`@org/name/dist/${value}.js`);

Solution: for internal source files, add the file(s) to the entry patterns. For external dependencies, add the dependency to the ignoreDependencies list.

Knip does not support extensionless imports for some non-standard extensions, such as for .svg files. Bundlers like Webpack may support this, but Knip does not. Here’s an example:

App.vue
import Component from './Component'; // → Should resolve to ./Component.vue
import ArrowIcon from '../icons/Arrow'; // → Does NOT resolve to ../icons/Arrow.svg

The first import is resolved properly, because .vue is a known extension if the Vue plugin is enabled. The second import might not be resolved, because .svg is not a known extension.

The recommendation is to always add the extension when importing such files, similar to how standard ES Modules specifies file extensions are necessary.

Knip considers TS config path aliases and paths configured in knip.json, but not those in e.g. Webpack or Vite configurations.

Solution: configure paths or try relative imports. Otherwise, use ignoreUnresolved as a last resort.

External libraries may use aliased imports that aren’t resolved by Knip.

For instance, unplugin-icons does this to import icons from icon sets as components. Such imports are reported as unused. Use the paths configuration option to tell Knip where to find the icon types:

knip.json
{
"paths": {
"~icons/*": ["node_modules/unplugin-icons/types/[framework].d.ts"]
}
}

Where [framework] is the name of the framework you’re using (see available types).

Solution: try —include-libs or configure paths.

By default, Knip does not report unused exports of entry files.

The most common causes for unused exports include:

Use the --exports flag to filter and focus only on issues related to exports:

Terminal window
knip --exports

Use includeEntryExports to report unused exports of entry files as well. This can be set per workspace.

For exports on an imported namespace, Knip considers all exports referenced if that namespace is used in certain patterns like enumeration. Individual exports are then not reported.

Solution: if all exports on imported namespaces should be considered individually, include the nsExports issue type to disable the heuristic.

See namepace imports to see all related patterns.

Are the exports consumed or imported by an external library, resulting in a non-standard consumption of your exports? Here’s an example:

import loadable from '@loadable/component';
export const DynamicApple = dynamic(() =>
import('./components.js').then(mod => mod.Apple)
);
export const LoadableOrange = loadable(() => import('./components.js'), {
resolveComponent: components => components.Orange,
});

Knip understands Apple is used, since it’s standard usage. But Orange is referenced through a function of an external library. For performance reasons, Knip does not include external type definitions by default so it won’t see the export being referenced.

Solution: include the type definitions of external libraries with the —include-libs flag:

Terminal window
knip --include-libs

This comes at a performance and memory penalty, but should give better results if you need it. This flag is implied when classMembers are included (that feature comes with roughly the same performance penalty).

To exclude false positives from the report, there are a few options:

  • Ignore exports used in file for exports used internally.
  • Individual exports can be tagged using JSDoc syntax.
  • Have the export in an entry file:
    • Add the file to the entry file patterns array in the configuration.
    • Move the export(s) to an entry file.
    • Add the file to the exports field of package.json
  • Re-export the unused export(s) from an entry file.

Did you expect certain exports in the report, but are they missing? They might be exported from an entry file. In that case, use —include-entry-exports to make Knip also report unused exports in entry files.

The exports of non-standard extensions like .astro, .mdx, .vue or .svelte are not available by default. See compilers for more details on how to include them.

Unused members of exported classes are not reported by default, here’s how to enable them:

Terminal window
knip --include classMembers

This option is also available in the Knip configuration file. Note that this feature comes at a cost: linting will take more time and more memory.

Individual class members can be tagged using JSDoc syntax.

Classes exported from entry files are ignored, and so are their members. Use —include-entry-exports to make Knip also report members of unused exports in entry files.

Unused enums and unused members of exported enums are reported by default. Reporting such members can be disabled:

Terminal window
knip --exclude enumMembers

Individual enum members can be tagged using JSDoc syntax.

Enums exported from entry files are ignored, and so are their members. Use —include-entry-exports to make Knip also report members of unused exports in entry files.

If you believe Knip incorrectly reports something as unused (i.e. there’s a false positive), feel free to create a minimal reproduction and open an issue on GitHub. It’ll make Knip better for everyone! Join the Discord channel to discuss any feedback or questions you may have.

ISC License © 2024 Lars Kappert