Skip to main content

Command Palette

Search for a command to run...

Delete Code with Knip

Updated
โ€ข9 min read
Delete Code with Knip
K

I am passionate about Front End development in React using frameworks and tools like Remix, TypeScript and Tailwind.

One of my favorite things to do as a software developer is delete code. Nothing is more satisfying than deleting hundreds of thousands of lines of code that are no longer needed in your application. Even if it's only 5 lines of code, I still get all warm and fuzzy from it.

One of the benefits of removing code is you no longer have to maintain it. It's just gone. You'll never ask yourself, "Is this code still needed?" if it doesn't exist. Have you ever opened up your codebase and found some code written way too long ago and wondered if it was safe to remove? It's kind of like this:

"Not sure if it's safe to remove this bit, so we'll leave it there just in case."
- some developer

Now the magic question is, how do we find code to remove? Insert Knip

What is Knip?

Now normally when you are browsing the Twitter ๐• developer crowd you typically end up with a bunch of hot takes, arguments over whose framework sucks, blah blah blah. But for every 50 hot takes, occasionally you will land on something useful posted on ๐•. That's where I first heard about Knip. The creators describe it as:

"Knip finds unused files, dependencies and exports in your JavaScript and TypeScript projects. Less code and dependencies lead to improved performance, less maintenance and easier refactorings."

Now this isn't the first tool of its kind. Many different languages and frameworks have similar tools to help you find unused code. The trick is helping Knip know more about your project so it can help you find all those magical lines of code just waiting to be deleted.

An Existing Complex Project

For the rest of this article, I'll talk about the steps I've taken on a project at work to get it up and running using Knip. Due to the private nature of the project, I'll be focusing more on Knip and less on the decisions that were made on the project itself.

Initial Setup and Run

First thing first, let's install it as a dev dependency, add it as a script in our package.json and just run it and see what happens.

Install Knip:

npm install knip --save-dev

Updated our package.json scripts:

{
  "scripts": {
    "knip": "knip"
  }
}

Now let's run it and see what happens:

npm run knip

Now if you have any sizable project, once this finishes turning the numbers, you'll likely get a giant list of unused things to delete. Our project had so many things that my terminal in vscode wouldn't even show me the whole thing. It was massive.

False Positives & Default Configuration

Skimming the list of what has been reported by Knip, I learned quickly that a huge amount of the findings were NOT safe to delete. In fact, tons of what was shown there were things I knew were being used everywhere. Let's check out the default configuration for Knip.

A simplified example of their defaults from their documentation is:

{
  "entry": ["index.js", "src/index.js"],
  "project": ["**/*.js"]
}
๐Ÿ’ก
This is a simplification of their defaults. There's a lot more to them that you can read here. They try very hard to automatically detect what frameworks and libraries you're using to help out. They have a very cool plugin system that helps which we'll talk about later in this article.

Our project at work is built with Remix. If you've used that framework before you'll immediately know that Remix has several entry files: "root.tsx", "entry.client.tsx" and "entry.server.tsx" files along with every single route file.

Fortunately, Knip has several plugins that get automatically detected for us and used to make it smarter for understanding your code. It even has a plugin for Remix.

Their Remix plugin will be enabled when it finds any dependencies or dev dependencies that include @remix-run in the name. If it detects Remix, it will add a whole load of entry files:

{
  "remix": {
    "entry": [
      "remix.config.js",
      "remix.init/index.js",
      "app/root.tsx",
      "app/entry.{client,server}.{js,jsx,ts,tsx}",
      "app/routes/**/*.{js,ts,tsx}",
      "server.{js,ts}"
    ]
  }
}

This is fantastic if your project doesn't modify any of the default conventions in Remix, it should just work. But what do you do if you've heavily modified your Remix routing conventions? Well, you're in luck because our project does exactly that.

Disabling Built-In Plugins

Our project at work customizes how routing works in Remix because of its scale and product requirements. Typically with Remix, you'll see an "app" folder that contains other folders like "components", "routes", etc. We've customized our routing to be split into logical modules that our product team has defined. You can think of these are features customers can turn on and off depending on what they pay for. We also have a few modules that are dedicated to authentication, core sections that all customers get, and even shared components that are used across multiple modules.

So an example of our file structure may look roughly like this:

app
โ”œโ”€โ”€ auth
โ”‚   โ”œโ”€โ”€ helpers
โ”‚   โ”œโ”€โ”€ routes
โ”‚   โ””โ”€โ”€ services
โ”œโ”€โ”€ core
โ”‚   โ”œโ”€โ”€ components
โ”‚   โ”œโ”€โ”€ helpers
โ”‚   โ””โ”€โ”€ routes
โ”œโ”€โ”€ module-1
โ”‚   โ”œโ”€โ”€ components
โ”‚   โ”œโ”€โ”€ helpers
โ”‚   โ”œโ”€โ”€ routes
โ”‚   โ””โ”€โ”€ utils
โ”œโ”€โ”€ module-2
โ”‚   โ”œโ”€โ”€ components
โ”‚   โ””โ”€โ”€ routes
โ””โ”€โ”€ shared
    โ””โ”€โ”€ compoents

As you as see, the Remix plugin for Knip isn't exactly what we want, so we need to disable it and configure new entry points for our application. We'll create a knip.json file in the root to use as our config. We'll also add a "$schema" value at the top to help vscode give us some intellisense.

{
  "$schema": "https://unpkg.com/knip@2/schema.json",
  "remix": false,
  "entry": [
    "server.js",
    "remix.config.js",
    "app/root.tsx",
    "app/entry.{client,server}.tsx",
    "app/{auth,core,module-1,module-2}/routes/**/{index,_layout}.{ts,tsx}",
    "app/{auth,core,module-1,module-2}/routes/*.{ts,tsx}",
    "app/core/routes/__global-layout.tsx"
  ],
  "project": [
    "remix.config.js", 
    "server.js", 
    "module-routes.js", 
    "app/**/*.{tsx,ts}"
  ],
}

Let's walk through each section and give a quick summary of what we're doing here.


  "remix": false,

This disables their built-in Remix plugin.


  "entry": [
    "server.js",
    "remix.config.js",
    "app/root.tsx",
    "app/entry.{client,server}.tsx",
    "app/core/routes/__global-layout.tsx"
    "app/{auth,core,module-1,module-2}/routes/*.{ts,tsx}",
    "app/{auth,core,module-1,module-2}/routes/**/{index,_layout}.{ts,tsx}",
  ],

Here we've defined our new entry files. Most of this is similar to the Remix plugin until we get to the section about our modules and their routes. The other files should be easy to understand so I'll talk about the last 3.

app/core/routes/__global-layout.tsx

We have a global layout that has the primary navigation for our application that most modules use. We have configurations where an individual module can opt out of the global layout if they want to build some other kind of navigation or perhaps be used as an unauthenticated section.

app/{auth,core,module-1,module-2}/routes/*.{ts,tsx}

This entry includes any ts or tsx files that are in the root of our routes folder.

app/{auth,core,module-1,module-2}/routes/**/{index,_layout}.{ts,tsx}

This one is a bit more hairy without some context. To try not to bore you, I'll try to make this quick, we're using Kiliman's remix-flat-routes for our routing. We love being able to co-locate components, hooks, utilities, and any other files we need that are only used for a specific route. This helps us not bloat our other folders with code that is only used for a single page. Remix v2 by default uses a flat routes convention but we've opted into using Kiliman's flat routes library because of its ability to have "Nested folders" while still enjoying the flat files convention and co-locating our code.

Example using default flat routes with Remix:

routes
โ”œโ”€โ”€ admin
โ”œโ”€โ”€ admin._index
โ”œโ”€โ”€ admin.users._index
โ”œโ”€โ”€ admin.users.$userId._index
โ”œโ”€โ”€ admin.users.$userId.edit
โ”œโ”€โ”€ admin.users.create
โ”œโ”€โ”€ admin.settings._index
โ”œโ”€โ”€ some-route
โ”œโ”€โ”€ some-route._index
โ”œโ”€โ”€ some-route.nested-item._index
โ”œโ”€โ”€ some-other
โ””โ”€โ”€ some-other._index

Imagine having other 50+ routes. This is where the "Nested folders" come in. You can simply add group routes in another folder and as long as it's suffixed with "+" it won't be treated as a flat route but a grouping of flat routes.

Example using remix-flat-routes with "Nested folders":

routes
โ”œโ”€โ”€ admin+
โ”‚   โ”œโ”€โ”€ _index
โ”‚   โ”œโ”€โ”€ users._index
โ”‚   โ”œโ”€โ”€ users.$userId._index
โ”‚   โ”œโ”€โ”€ users.$userId.edit
โ”‚   โ”œโ”€โ”€ users.create
โ”‚   โ”œโ”€โ”€ settings._index
โ”‚   โ””โ”€โ”€ _layout.tsx
โ”œโ”€โ”€ some-route+
โ”‚   โ”œโ”€โ”€ _index
โ”‚   โ”œโ”€โ”€ nested-item._index
โ”‚   โ””โ”€โ”€ _layout.tsx
โ””โ”€โ”€ some-other+
    โ”œโ”€โ”€ _index
    โ””โ”€โ”€ _layout.tsx

Now when I'm looking at our routes, I'm greeted with fewer folders that group my routes in logical ways.


  "project": [
    "remix.config.js", 
    "server.js", 
    "module-routes.js", 
    "app/**/*.{tsx,ts}"
  ],

Our project root has lots of different folders. Several of those folders are strictly for our QA team for them to write automation that is separate from our application. So we're defining all the files we want Knip to check. We've got 3 root files then everything else is every file under our "app" directory.

Let's run Knip again and see if our list is any better. Spoiler alert, it's not yet. We have a MASSIVE list still, many falling under Unused exported types or Unused exported types in namespaces.

Unused Exported Types

397 unused exported types. That's a lot of "unused" exported types. Let's first talk about what this is, why my project has so many of them, and what we can do about it.

What is an unused exported type?

Hopefully, the phrase speaks for itself, but it is a TypeScript type or interface that is being exported, but is never imported into another file.

export type FavoriteColor = 'red' | 'blue' | 'green'

export interface Person {
  name: string
  age: number
  favoriteColor: FavoriteColor
}

/** 
 * Some other code in the same file, that may or may not reference 
 * our `FavoriteColor` or `Person` types. Note no other files 
 * reference these 2 types.
*/

If nothing in your code base is importing these types, then they will be listed by knip.

Why does my project have so many of these?

Our project has a pattern for how we create React components. Here's an example below.

export interface ButtonProps {
  // button props here
}

export const Button = (props: ButtonProps) => {
  // return button here
}

When we make a component, we export the component itself and that gets used throughout our application. Along with each component, we define an interface with its properties. We also export this type, in cases where another component that consumes it, wants to use its prop type.

After you've built hundreds of components like this, it makes sense why knip has such a large list of unused exports. Now what to do about it?

What are my options to remove these unused exported types?

The obvious answer is to stop exporting it. Boom problem solved. But that wasn't a solution that I was happy with because I'm a stubborn developer and I exported the type for a reason. That reason was "just in case" a consumer wanted to use it.

So now what? Luckily there are some configuration options we can use with Knip.

{
  "ignoreExportsUsedInFile": {
    "interface": true,
    "type": true,
    "enum": true
  },
}

You can either set ignoreExportsUsedInFile for everything by setting that property to true or you can opt into certain things being ignored. As you can see from the example above. We've decided to ignore exports used in files for interfaces, types and enums.

Conclusion

We've made a lot of progress in getting Knip up and going on a fairly large project. We still have some more work to go but we're already starting to see some benefits and finding lots of unused code. You know what unused code means... I get to delete it!