Use import type for type-only imports
rule · type-only-imports
TypeScript's import type (opens in a new tab) syntax explicitly marks an import as type-only, guaranteeing that the compiler erases it completely from the emitted JavaScript with no module evaluation.
Code Examples
// ❌ Regular import used only for a type — module is evaluated at runtime
import { User } from './user' // './user' module code runs
import { ApiResponse } from './api' // './api' module code runs
function formatUser(user: User): string {
return user.name
}
async function handleResponse(res: ApiResponse<User>): Promise<User> {
return res.data
}// ✅ import type — both imports are completely erased from the output JS
import type { User } from './user'
import type { ApiResponse } from './api'
function formatUser(user: User): string {
return user.name
}
async function handleResponse(res: ApiResponse<User>): Promise<User> {
return res.data
}The compiled output for the type-only version contains no reference to ./user or ./api. No module evaluation, no bundle inclusion.
Why It Matters
Regular imports cause the JavaScript runtime to evaluate and execute the imported module even when only a TypeScript type from that module is used. This can increase bundle size, introduce unintended side effects, and create circular dependency issues that are otherwise avoidable. Using import type makes the erased-at-runtime nature of type imports explicit and enables bundlers and the TypeScript compiler to handle them optimally.
Mixed Imports — Types and Values Together
When you need both a type and a runtime value from the same module, you can either use a single import with inline type modifiers, or separate the imports:
// Option A — inline type modifier (TypeScript 4.5+)
import { createUser, type User, type CreateUserOptions } from './user'
// ^^^^ type ^^^^ type — erased at compile time
// ^^^^^^^^^^^ value — kept in runtime output
// Option B — two separate imports
import { createUser } from './user' // runtime value
import type { User, CreateUserOptions } from './user' // types only
// Both are correct — choose based on team preferenceverbatimModuleSyntax — Automatic Enforcement
TypeScript 5.0 introduced verbatimModuleSyntax, which enforces that type-only imports use import type. The compiler will error if you write a regular import that is only used as a type.
// tsconfig.json
{
"compilerOptions": {
"verbatimModuleSyntax": true,
// verbatimModuleSyntax replaces the older importsNotUsedAsValues and
// preserveValueImports options — do not set those alongside it
"module": "ESNext",
"moduleResolution": "bundler"
}
}With verbatimModuleSyntax (opens in a new tab) enabled, this becomes a compiler error:
// ❌ TypeScript error: This import is never used as a value and must use 'import type'
import { User } from './user'
function greet(user: User): string {
return `Hello, ${user.name}`
}ESLint Rule
Add the consistent-type-imports rule (opens in a new tab) to enforce import type via your linter, independently of TypeScript version:
// eslint.config.js (flat config)
{
"rules": {
"@typescript-eslint/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
]
}
}The rule is auto-fixable — running eslint --fix converts all violating imports automatically.
Re-exporting Types
Type-only exports follow the same pattern:
// types.ts
export type { User } from './user'
export type { ApiResponse, ApiError } from './api'
// Both are erased from the compiled output of types.ts
// Consumers of types.ts still get the full typesType Imports in Declaration Files
In .d.ts files, all imports are already type-only by nature, but using import type is still considered good practice as it makes intent explicit:
// declarations.d.ts
import type { EventEmitter } from 'events'
declare class MyEmitter extends EventEmitter {
on(event: 'data', listener: (chunk: Buffer) => void): this
}Verification
The TypeScript 3.8 type-only import notes (opens in a new tab) and the consistent-type-imports rule (opens in a new tab) are the fastest way to verify both compiler and linter behavior agree on the same import boundary.
- Run
pnpm exec tsc --noEmitand confirm zero errors. If usingverbatimModuleSyntax: true, any type-only import withoutimport typewill surface as a compiler error. - Run the ESLint
consistent-type-importsrule across thesrc/directory and confirm zero violations (the rule is auto-fixable with--fix). - Inspect the compiled JavaScript output for a module that uses only type imports — confirm the imported module paths do not appear in the output file.
- Search for
import {in TypeScript files and verify each imported identifier is actually used as a runtime value, not only in type positions.