Right, let’s talk about one of the single biggest “aha!” moments for speeding up your TypeScript compilation: import type. This isn’t just a fancy syntax; it’s a cheat code that tells the TypeScript compiler, “Hey, relax, we’re not actually going to run this. I just need to know what it looks like.”

Think of your average import statement as ordering a full, assembled piece of IKEA furniture. The delivery truck (your bundler) shows up, you get the massive box (the module’s code), and you have to haul it inside and put it together (include it in your JavaScript output). Now, imagine if you could just call the company, describe the furniture, and have them fax you the assembly instructions (the type definitions) without the physical box ever showing up at your door. That’s what import type does. It only brings over the type information and leaves absolutely no trace in your final JavaScript. This is a huge win because it reduces the amount of code the compiler and your bundler have to process.

Why This Speeds Things Up

The magic happens in two places: during type checking and, more importantly, during declaration emit.

When you use a normal import { SomeType } from './my-module', TypeScript has to go find ./my-module, figure out what it exports, and then determine what SomeType is. But it also has to include that ./my-module module in the graph of dependencies that might have side effects. Your bundler then has to include it too, because for all it knows, my-module might console.log something important when it’s imported.

When you use import type { SomeType } from './my-module', you’re making a promise to the compiler. You’re saying, “I swear, I only care about the shape of SomeType. I don’t need any of the runtime code from my-module.” The compiler believes you. It goes and grabs the type info it needs for checking and then completely forgets about the import statement. It won’t be in your .js output, and it won’t be a dependency for your bundler. You’ve just trimmed a node from your dependency tree.

The real performance killer, though, is when you’re also generating declaration files (.d.ts) with "declaration": true in your tsconfig.json. For a normal import, the compiler now has to figure out if that imported value is a type or a value to decide what to emit in the .d.ts file. This process, called “declaration emit,” involves a non-trivial amount of work. By using import type, you short-circuit this entire process. You’ve already told the compiler it’s only a type, so it doesn’t have to do any extra analysis. It just inlines the type shape and moves on.

How to Use It (Without Losing Your Mind)

The syntax is straightforward. Swap your runtime imports for type-only ones.

// BEFORE: This brings in both type and code
import { MyInterface, someRuntimeFunction } from './other-file';

// AFTER: This only brings in the type. No runtime code is imported.
import type { MyInterface } from './other-file';
import { someRuntimeFunction } from './other-file';

You can also use the inline import syntax for types, which is wonderfully explicit.

// This function requires an object that matches the shape defined in './types'.
export function processUser(user: import('./types').User): void {
  // ... implementation
}

A common pitfall? Trying to use something imported only as a type in a value context. The compiler will instantly call you out, and rightly so.

import type { SomeClass } from './some-class';

// ERROR: 'SomeClass' cannot be used as a value because it was imported using 'import type'.
const instance = new SomeClass();

This is the compiler saving you from a runtime error. Remember, SomeClass doesn’t exist in your JavaScript world. You only imported its ghost.

When You Can Go Full importsNotUsedAsValues

If you’re staring at a file and realizing every single import is only being used for types, you might be tempted to just use a normal import and let the compiler remove them. This is where the importsNotUsedAsValues flag in tsconfig.json comes in, with its three modes:

  • "remove" (default): The classic behavior. Removes the import statement if the imports are only used as types.
  • "preserve": Keeps the import statement, which is useful for modules that have side effects (like polyfills).
  • "error": This is the pedantic, performance-enforcing mode. It will error if you use a regular import for something that’s only used as a type. It forces you to explicitly use import type.

I’m a fan of "error" in strict codebases. It makes the intent crystal clear and guarantees you’re not accidentally dragging in runtime modules where you don’t need them. It turns a potential performance optimization into a hard requirement.

So, start auditing your imports. Every time you replace a regular import with import type, you’re not just being pedantic—you’re giving the compiler a free pass to skip a chunk of work. And in a large codebase, those skipped chunks add up to a compilation time that feels downright snappy.