import type

Imports a binding for type positions only, guaranteeing it is erased from the emitted JavaScript.

Signature
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

ParameterTypeDescription
$TidentifierThe exported type, interface, class or enum being bound. Under `import type` the binding is legal only in type positions.
$"mod"string literalThe module specifier, resolved exactly as it would be for a normal import declaration.
$* as nsoptionalnamespace bindingBinds the module's type namespace, so `ns.Foo` names a type. The namespace object itself never exists at runtime.
$type (inline modifier)optionalkeywordPlaced 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 isolatedModules or a single-file transpiler (esbuild, swc, Babel, Vite). Those tools compile one file at a time and cannot know whether Foo was an interface, so they either keep a dead import or drop a live one. The type keyword 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

Example · typescript
// 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.

Example · typescript
// 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.

Example · typescript
// 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

  • Be the first to comment on this lesson.