Right, let’s talk about import type. This is one of those features that feels like a bureaucratic formality until the moment it saves you from a truly spectacular runtime error. It exists for one simple, beautiful reason: to help TypeScript do its job without getting its hands dirty with JavaScript’s messy runtime business.

Here’s the core idea: a normal import statement does two things. It tells TypeScript “I need this type for my type-checking,” and it tells your JavaScript bundler or runtime “I need this code to be included and executed.” Most of the time, this is exactly what you want. But sometimes, you only care about the type information. You don’t want any JavaScript code generated for that import. This is where import type comes in. It’s a firm, clear instruction to the TypeScript compiler: “Hey, just get the type info from this file and then leave it alone. Don’t emit any require statements for it.”

Why Bother? The Circular Dependency Nightmare

The most classic, hair-pulling reason to use import type is to break a circular dependency. Imagine two files, each thinking they’re the most important thing in the universe:

// author.ts
import { Book } from './book';

export interface Author {
  name: string;
  books: Book[]; // We need the Book type
}

export function createAuthor(name: string): Author {
  return { name, books: [] };
}
// book.ts
import { Author } from './author';

export interface Book {
  title: string;
  author: Author; // We need the Author type
}

export function createBook(title: string, author: Author): Book {
  return { title, author };
}

See the problem? author.ts depends on book.ts, which depends on author.ts. If you’re using a module system that executes these imports at runtime (like CommonJS), this can cause a world of pain, resulting in undefined values. Even if it doesn’t, it makes your code smell funny. The fix is obvious once you see it: neither file actually needs the value from the other—they just need the type. This is a job for import type.

// book.ts (fixed)
import type { Author } from './author'; // <-- The magic words

export interface Book {
  title: string;
  author: Author;
}

export function createBook(title: string, author: Author): Book {
  return { title, author };
}

Now, the circular dependency is broken at the JavaScript level. The generated JS for book.js will have no require statement for ./author. TypeScript gets the Author type it needs for checking, and the runtime avoids the awkward dance of two modules trying to load each other simultaneously.

It’s Not Just for Circles: The Bundler Benefit

Even without circular dependencies, using import type is a best practice for things you know are purely type-based. Think about utility type libraries or massive enum definitions that only exist for TypeScript’s benefit.

// Before: Clutters your final bundle
import { SomeBigUtilityType } from './type-utils';
import { MyEnum } from './enums';

const myVar: SomeBigUtilityType = { ... };

// After: Clean and purposeful
import type { SomeBigUtilityType } from './type-utils';
import type { MyEnum } from './enums';

const myVar: SomeBigUtilityType = { ... };

When you build your project, the JavaScript output for the “After” example will be completely free of any reference to ./type-utils or ./enums. Your bundler (Webpack, Rollup, etc.) won’t even bother looking at them, leading to slightly faster builds and slightly leaner bundles. It’s a small win, but it adds up and makes your intent crystal clear to anyone reading the code.

The Syntax: You’ve Got Options

TypeScript gives you a few ways to write this, because of course it does. You can do it inline or as a standalone statement.

Standalone import type:

import type { SomeType } from 'some-module';

Inline type import:

import { type SomeType, someActualValue } from 'some-module';

The inline syntax is fantastic. It allows you to mix and match: grab a real, runtime value and a type from the same module in a single, clean line. The compiler is smart enough to only emit the JavaScript for someActualValue and treat SomeType as a type-only import.

What You Can and Cannot Do

This is the crucial part. import type can only be used in places that are erased during compilation. You cannot use it for anything that requires a runtime value.

This is perfectly fine:

import type { MyType } from './types';
let x: MyType; // Using it as a type annotation

This will cause a compiler error:

import type { MyClass } from './my-class';
const instance = new MyClass(); // 💥 Error: 'MyClass' cannot be used as a value because it was imported using 'import type'.

The error message is brilliantly clear. You told TypeScript that MyClass was just a type, so it threw away all the JavaScript needed to actually construct it. You can’t have it both ways. If you need to use something as a value, you must use a regular import.

The rule of thumb is simple: if you’re only using the imported name in a type position (after a : or in a generic), use import type. If you’re using it in a value position (as an expression, with new, etc.), use a regular import. When in doubt, use import type; the compiler will yell at you if you’re wrong, which is far safer than the opposite.