import type
Imports a binding for type positions only, guaranteeing it is erased from the emitted JavaScript.
import type { T } from "mod";
import type T from "mod";
import type * as ns from "mod";
import type { default as T, Other } from "mod";
import { value, type T } from "mod";- Added in TypeScript 3.8
Parameters
| Parameter | Type | Description |
|---|---|---|
$T | identifier | The exported type, interface, class or enum being bound. Under `import type` the binding is legal only in type positions. |
$"mod" | string literal | The module specifier, resolved exactly as it would be for a normal import declaration. |
$* as nsoptional | namespace binding | Binds the module's type namespace, so `ns.Foo` names a type. The namespace object itself never exists at runtime. |
$type (inline modifier)optional | keyword | Placed before an individual specifier inside a normal import, marking just that specifier as type-only while the rest of the declaration still emits. |
Errors
Using a `import type` binding in a value position raises "'X' cannot be used as a value because it was imported using 'import type'", and combining a default import with named bindings in one type-only declaration raises "A type-only import can specify a default import or named bindings, but not both."
Changelog
- PHP 3.8`import type` introduced, alongside the `importsNotUsedAsValues` flag.
- PHP 4.5Inline `type` modifiers on individual specifiers (`import { a, type B }`) added, so one declaration can carry both values and types.
- PHP 5.0`verbatimModuleSyntax` replaces `importsNotUsedAsValues` and `preserveValueImports`: anything without a `type` modifier is now emitted verbatim, so the modifier becomes the only way to drop an import.
A plain import is ambiguous: the compiler has to look at every use of the binding to decide whether the import survives into JavaScript. import type removes the guesswork. The declaration is erased unconditionally, and the binding is legal only where a type is expected.
When to reach for it
- Under
isolatedModulesor a single-file transpiler (esbuild, swc, Babel, Vite). Those tools compile one file at a time and cannot know whetherFoowas an interface, so they either keep a dead import or drop a live one. Thetypekeyword tells them outright. - To break an import cycle. Two modules that only need each other's types stop being runtime dependencies once both imports are type-only.
- To avoid pulling in a side-effecting module just to name one of its interfaces.
What trips people up
Importing a class with import type gives you the instance type but not the constructor, so new Foo() and extends Foo both fail. If you need both the type and the value, use a normal import, or the inline form: import { Foo, type FooOptions } from "./foo.js".
Note also the direction of the modifier. import type * as ns makes the whole namespace type-only; import * as ns with an inline type inside is not a thing — the inline modifier only applies to named specifiers.
Example
// user.ts
export interface User { id: number; name: string }
export function greet(u: User): string { return `Hello, ${u.name}`; }
// main.ts
import { greet, type User } from "./user.js"; // inline modifier: greet emits, User does not
import type { User as Person } from "./user.js"; // whole declaration erased
const ada: User = { id: 1, name: "Ada" };
const alias: Person = ada;
console.log(greet(alias));
// -> "Hello, Ada"
// Emitted JS keeps only: import { greet } from "./user.js";When to use it
- Import an interface from a module whose side effects you do not want to trigger
- Satisfy isolatedModules or verbatimModuleSyntax when esbuild or swc does the transpiling
- Break a runtime circular dependency between two modules that only share types
- Mix a runtime helper and its options interface in a single import statement
Frequently asked questions
What is the difference between import type and a regular import?
`import type` is guaranteed to be erased from the JavaScript output, while a regular import may or may not be, depending on how the binding is used and which compiler flags are set. A regular import also gives you the runtime value, so you can call the function or extend the class; a type-only import gives you only the type.
Why do I get "cannot be used as a value because it was imported using import type"?
You used a type-only binding in a position that produces JavaScript — calling it, constructing it with `new`, extending it, or reading a static member. Change the declaration to a normal import, or use the inline form `import { Thing, type ThingOptions }` so the value survives and only the type is dropped.
Do I need import type if I use isolatedModules?
Yes, for anything that is purely a type. Under `isolatedModules` each file is compiled alone, so the transpiler cannot tell an interface from a class and will leave the import in the output, producing a runtime error for a binding the module never exported. The `type` keyword makes the intent explicit in the file itself.
More examples
Breaking a circular dependency
Both files reference each other's types, but neither emits an import, so at runtime there is no cycle at all — order.js and customer.js load independently. Had these been plain imports under `verbatimModuleSyntax`, the two `import` statements would survive and the module graph would contain a real loop, with one side seeing a half-initialised module.
// order.ts
import type { Customer } from "./customer.js";
export interface Order {
id: string;
placedBy: Customer;
}
export function describe(o: Order): string {
return `${o.id} for ${o.placedBy.name}`;
}
// customer.ts
import type { Order } from "./order.js";
export interface Customer {
name: string;
history: Order[];
}
const c: Customer = { name: "Ada", history: [] };
const o: Order = { id: "A-1", placedBy: c };
c.history.push(o);
console.log(describe(o));
// -> "A-1 for Ada"Type-only import of a value, and where it stops working
The annotation `l: Logger` and the constructor type `new (...) => Logger` both compile, because they are type positions. The commented `new Logger(...)` is the line tsc rejects. This is the usual reason a working import "breaks" after someone adds the `type` keyword to tidy up: the annotation still resolves, so the error appears far from the edit.
// logger.ts
export class Logger {
constructor(public prefix: string) {}
write(msg: string) { console.log(`${this.prefix}: ${msg}`); }
}
// consumer.ts
import type { Logger } from "./logger.js";
// OK — type positions only
function useLogger(l: Logger): void { l.write("ready"); }
type Factory = new (prefix: string) => Logger;
// tsc: 'Logger' cannot be used as a value because it was imported using 'import type'.
// const bad = new Logger("app");
const make: Factory = class extends Object {
constructor(public prefix: string) { super(); }
write(msg: string) { console.log(`${this.prefix}: ${msg}`); }
} as unknown as Factory;
useLogger(make("app"));
console.log("annotated with Logger, constructed without it");
Discussion