Modules & type imports
Export types from one file and import them in another, see why import type exists, what verbatimModuleSyntax checks, and how a module differs from a script.
- 01Share types between filesExport a type alias or an interface from one file and import it in another, and read the errors when an export is missing.
- 02Mark imports that are only typesUse
import typeandimport { type … }, and say what each one leaves in the JavaScript the browser runs. - 03Use verbatimModuleSyntax and modulesExplain why build tools need types marked, fix the errors
verbatimModuleSyntaxreports, and turn a script into a module.
Types that travel between files
A shop app isn't one long file. One file describes a product, another shows the cart, another handles checkout. In JavaScript, files share values with export and import (the import and export lesson covers how). In TypeScript, files share types the same way. Here is a file that describes a product:
export type Product = { name: string; price: number }; export interface CartItem { product: Product; quantity: number;} export function formatPrice(price: number) { return "$" + price.toFixed(2);}Line 1 exports a type alias called Product: a name and a price. Lines 3 to 6 export an interface called CartItem: a product and how many of it. Lines 8 to 10 export a function, formatPrice, that turns a price like 3 into the text $3.00. The word export works the same in front of all three. Another file can now use them:
import { Product, formatPrice } from "./product"; const tea: Product = { name: "Tea", price: 3 };console.log(tea.name, formatPrice(tea.price));Line 1 imports Product and formatPrice from "./product", which means the file product.ts in the same folder. You leave out the .ts, as you would in a project built with a tool like Vite. Line 3 uses Product as the type of tea, so TypeScript checks that the tea has a name and a price. Line 4 calls formatPrice and prints Tea $3.00. Point at Product on line 1: TypeScript shows the type it brought in from product.ts. Press Edit & run, and cart.ts runs with product.ts beside it.
product.ts exports two types and a function. cart.ts imports the Product type and the formatPrice function with one import, the same way for both.One import brought in two very different things. formatPrice is a value: real code that the browser loads from product.js and runs. Product is only a type. Types are removed before the browser runs anything (the How TypeScript works lesson shows how), so product.js has no Product in it at all. This lesson is about what that means for your imports.
You order a sofa. First the shop sends you its catalog page: the size and the color. You use the page to plan where the sofa goes. On delivery day, the truck brings the sofa itself. Nobody puts the catalog page on the truck; you only needed it for planning. And if you only want the page, you tick "catalog page only", and no truck comes.
- In real life: The shop sends you the catalog page for a sofa
- In TypeScript: product.ts exports the
Producttype - In real life: You use the page to plan your room
- In TypeScript: TypeScript uses the type to check cart.ts
- In real life: The truck brings the sofa, never the page
- In TypeScript: The browser gets values like
formatPrice, never types - In real life: You tick "catalog page only" on the order form
- In TypeScript: You write
import type, so nothing is loaded for it
A file with import or export is a module, and modules share types just as they share values. Types are only for checking, so they're removed from the JavaScript. import type marks an import that brings only types, so every tool can remove it safely.
Exporting and importing types
To share a type, put export in front of it, as product.ts does on lines 1 and 3. To use it, import it by name. Here is a checkout file that uses the CartItem interface:
import { CartItem, formatPrice } from "./product"; const item: CartItem = { product: { name: "Cake", price: 4 }, quantity: 2,};const total = item.product.price * item.quantity;// ^?console.log(formatPrice(total));Line 1 imports the CartItem interface and the formatPrice function. Lines 3 to 6 make an item that must match CartItem: a cake at 4, twice. Line 7 multiplies the price by the quantity, and the query on line 8 shows const total: number. Line 9 prints $8.00. Inside checkout.ts, an imported type works exactly like a type written in the same file.
Two things follow. A type without export stays private to its file. And when a type changes, every file that imports it is checked against the new version. Try both: each button changes line 1 of product.ts, and TypeScript checks cart.ts against it.
export type Product = { name: string; price: number }; export interface CartItem { product: Product; quantity: number;} export function formatPrice(price: number) { return "$" + price.toFixed(2);}import { Product, formatPrice } from "./product"; const tea: Product = { name: "Tea", price: 3 };console.log(tea.name, formatPrice(tea.price));export typeNo errorsprints Tea $3.00Line 1 exports the Product type, so cart.ts can import it. Its tea has a name and a price, so TypeScript has nothing to report.
No export keeps Product inside product.ts. cart.ts gets Module '"./product"' declares 'Product' locally, but it is not exported, which means "product.ts has a Product, but keeps it to itself". export interface works just like export type: the importing file doesn't care which one it gets. Remove price changes only product.ts, yet TypeScript marks lines 3 and 4 of cart.ts. In every state the code still prints Tea $3.00, because types don't change what runs.
Every export is a type, a value, or both, and that decides what's left of it in the JavaScript. Read each row as: what a file exports, which kind of thing it is, and what the browser gets.
| What the file exports | A type or a value? | What's left in the JavaScript |
|---|---|---|
export type Product = { … } | A type. | Nothing. The whole line is removed. |
export interface CartItem { … } | A type. | Nothing. The whole interface is removed. |
export function formatPrice(…) | A value: a function you can call. | The function, without its type annotations. |
export class ShoppingCart { … } | Both: a type you can write after a colon, and a value you can call with new. | The class, without its type annotations. |
The last row needs an example. A class makes both a type and a value with one name:
export class ShoppingCart { items: string[] = []; add(name: string) { this.items.push(name); }}Line 1 exports a class, ShoppingCart. Another file can write cart: ShoppingCart to use it as a type, and new ShoppingCart() to make a real cart when the code runs (the Classes in TypeScript lesson covers classes). Lines 2 to 5 give each cart a list of items and an add method. Exercise 6 imports it.
import type: an import that disappears
When TypeScript removes the types from cart.ts, what happens to line 1? Here is what the browser gets:
import { formatPrice } from "./product";
const tea = { name: "Tea", price: 3 };
console.log(tea.name, formatPrice(tea.price));Line 1 has lost Product. TypeScript saw that Product is only used as a type, in line 3's : Product, so it dropped the name from the import. That's exactly right: product.js has no Product to give. formatPrice stays, because line 4 calls it. Line 3 has lost its : Product too, and the code still prints Tea $3.00.
You can also say it yourself. import type is an import that brings only types:
import type { Product } from "./product";import { formatPrice } from "./product"; const tea: Product = { name: "Tea", price: 3 };console.log(tea.name, formatPrice(tea.price));Line 1 imports Product with import type. Line 2 imports formatPrice with a plain import, because it's a value. Checking doesn't change: line 4 must still be a Product. When the types are removed, line 1 goes away completely, and the code prints Tea $3.00.
import type line goes with them. The formatPrice import stays, because it brings a value, and : Product is removed from the tea.Why write type if TypeScript drops type imports anyway? Two reasons. Anyone reading line 1 knows at once that it loads nothing when the code runs. And some build tools can't work it out the way TypeScript did, so they need the mark; the next section shows them. The opposite mistake gets an error. Here formatPrice is imported with import type, on purpose:
import type { Product, formatPrice } from "./product"; const tea: Product = { name: "Tea", price: 3 };console.log(tea.name, formatPrice(tea.price));TypeScript marks line 4: 'formatPrice' cannot be used as a value because it was imported using 'import type'. In plain words: "you said this import brings only types, but you're calling it like a function". Run it anyway, and the whole of line 1 is gone, so it stops with ReferenceError: formatPrice is not defined.
There's a shorter way to mix the two: put type in front of one name inside the braces, import { type Product, formatPrice } from "./product";. Try every form yourself. Type line 1 of cart.ts and watch the list under the code:
import { Product, formatPrice } from "./product"; const tea: Product = { name: "Tea", price: 3 };console.log(tea.name, formatPrice(tea.price));import { formatPrice } from "./product";No errorsprints Tea $3.00Line 1 brings the Product type and the formatPrice function. No errors. The browser's copy keeps only formatPrice, because Product is a type.
With type after import, line 4 can't call formatPrice. With type in front of Product, there are no errors, and the browser's line 1 is import { formatPrice } from "./product";. Without Product in the braces, line 3 gets Cannot find name 'Product', and a misspelled path gets Cannot find module './products' or its corresponding type declarations.
The table below sums up the three forms. Each line ends in from "./product";, left out to keep the table short. Read each row as: the import line, what it brings, and what the JavaScript keeps.
| The import line | What it brings | What the JavaScript keeps |
|---|---|---|
import { Product, formatPrice } | A type and a value. | import { formatPrice }, because TypeScript works out that Product is a type and drops it. |
import type { Product } | Only types. | Nothing. The whole line is removed, and the file isn't loaded for it. |
import { type Product, formatPrice } | A type, marked with type, and a value. | import { formatPrice }, because the marked name is removed. |
verbatimModuleSyntax: what you write is what you get
The TypeScript compiler, tsc, reads every file in your project, so it knows that Product is a type. Many build tools work differently. Tools like Vite, esbuild, and SWC turn each .ts file into JavaScript on its own, one file at a time and without checking types, because that's fast (the Bundlers lesson explains what they do). A tool that sees one file can't always tell a type from a value. Here is a file where that goes wrong:
// shop.ts passes on what product.ts exports.export { Product, CartItem, formatPrice } from "./product";Line 2 passes on three names from product.ts, so other files can import them all from shop.ts. This is called a re-export. TypeScript reports no errors: tsc read product.ts and knows to drop the two types. A tool that sees only this file can't know which names are types, so it keeps line 2 exactly as it is. Press Edit & run: this page removes types one file at a time too, and the browser stops with a SyntaxError saying that the module doesn't provide an export named Product.
shop.ts, so it can't tell that Product is a type, and it keeps the line. The browser then looks for a Product export that product.js doesn't have.Here is why. This is product.ts after its types are removed. Only the function is left:
export function formatPrice(price) {
return "$" + price.toFixed(2);
}Lines 1 to 3 are the formatPrice function, without : number. There's no Product and no CartItem, so nothing can import them from it when the code runs.
The fix is a compiler option, verbatimModuleSyntax, which you turn on in tsconfig.json (the Setting up a project lesson covers that file). "Verbatim" means "word for word". With it on, the rule is simple: an import or export without type stays in the JavaScript exactly as you wrote it, and one with type is removed. A type must never reach the browser, so TypeScript now makes you mark every type. Here is shop.ts again with the option on:
// shop.ts passes on what product.ts exports.export { Product, CartItem, formatPrice } from "./product";The list under the code shows the option this example sets. TypeScript marks line 2 twice, once for each type, on purpose: Re-exporting a type when 'verbatimModuleSyntax' is enabled requires using 'export type'. In plain words: "this passes on a type, so write export type".
// shop.ts passes on what product.ts exports.export type { Product, CartItem } from "./product";export { formatPrice } from "./product";Line 2 passes on the two types with export type, and line 3 passes on formatPrice. There are no errors, and any tool can now tell what to remove: line 2 goes, line 3 stays. Now try the option on an import. Pick a way to write line 1 of cart.ts, then turn verbatimModuleSyntax on and off:
import { Product, formatPrice } from "./product"; const tea: Product = { name: "Tea", price: 3 };console.log(tea.name, formatPrice(tea.price));import { formatPrice } from "./product";offNo errorsWith the option off, TypeScript works out that Product is a type and drops it, so tsc writes import { formatPrice } from "./product";. No errors.
With the option off, the plain import is fine, because tsc drops Product for you. With it on, tsc keeps the line word for word, and TypeScript reports 'Product' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled: mark it with type. import { type Product, formatPrice } is fine either way, so it's the habit worth keeping. A new Vite project already turns verbatimModuleSyntax on in its tsconfig. An older option, isolatedModules, reports the same re-export error (the target, lib & module lesson covers it).
Modules and scripts
TypeScript, like JavaScript, has two kinds of files. A module has at least one import or export at the top level. A script has none. The difference is where the names a file declares at the top live (the Scope lesson explains scope). In a module, they stay inside the file. In a script, they go into the global scope, which every script on the page shares.
This site checks every example as a module unless it says otherwise, the way a new Vite project does with moduleDetection: "force". The next examples set moduleDetection: "auto", TypeScript's own default, so a file with no import or export is a script:
const name = "Asha";console.log("Hello, " + name);Lines 1 and 2 have no import or export, so this is a script, and name would be global. But every browser page already has a global name: the browser window's own name, also called window.name. TypeScript knows it from the browser's types and marks line 1 on purpose: Cannot redeclare block-scoped variable 'name' means "this scope already has a name, and const can't make a second one". The code still prints Hello, Asha when it runs, but the error is a sign the file isn't what you meant.
const name = "Asha";console.log("Hello, " + name); export {};Line 4, export {};, exports nothing. But one export is enough to make the file a module, so name now belongs to this file alone, and the error is gone. It prints Hello, Asha.
Two scripts clash with each other in the same way. Here is a checkout script on its own:
const total = 7;Line 1 declares total, and on its own the file is fine. Now a cart script in the same project:
const total = 3;console.log(total);Line 1 declares a total too. Both files are scripts, so both totals land in the same global scope, and TypeScript marks line 1: Cannot redeclare block-scoped variable 'total'. Add export {}; to either file and that file gets its own total.
total in the same shared place, so the names clash. Two modules each keep their own total, and nothing clashes.The table below compares the two kinds of file. Read each row as a question, then the answer for a module and for a script.
| Question | A module | A script |
|---|---|---|
| What makes a file one? | It has at least one import or export at the top level. | It has no import or export at all. |
| Where do its top-level names live? | Inside the file. Other files see only what it exports. | In one global scope that every script on the page shares. |
| How does the browser load it? | With <script type="module">, or through a bundler like Vite. | With a plain <script> tag. |
| How do you make it the other kind? | Remove every import and export. You rarely want to. | Add export {};, which exports nothing. |
One types file for a shop app
Real projects often keep the types that many files share in one file of their own, such as types.ts:
export type Product = { id: number; name: string; price: number }; export interface CartItem { product: Product; quantity: number;} export type Order = { items: CartItem[]; paid: boolean };Line 1 exports Product, now with an id. Lines 3 to 6 export CartItem, and line 8 exports Order: a list of cart items and whether it's paid. Every line is a type, so nothing in this file runs. tsc writes just export {}; for it: a module that exports nothing. Here is a file that uses it, in a project with verbatimModuleSyntax on:
import type { CartItem, Order } from "./types"; function orderTotal(order: Order) { let total = 0; for (const item of order.items) total += item.product.price * item.quantity; return total;} const items: CartItem[] = [ { product: { id: 1, name: "Tea", price: 3 }, quantity: 2 }, { product: { id: 2, name: "Cake", price: 4 }, quantity: 1 },];console.log(orderTotal({ items, paid: false }));Line 1 brings in CartItem and Order with import type, as the option requires. Lines 3 to 7 add up an order: each item's price times its quantity. Lines 9 to 12 hold two teas at 3 and one cake at 4, and line 13 prints 10. Because order.ts imports only types from types.ts, the browser never loads types.js at all. Now sort some import lines the way TypeScript would:
import type { Product } from "./product";import { Product } from "./product";import { type Product, formatPrice } from "./product";import type { formatPrice } from "./product"; formatPrice(3);import { formatPrice } from "./product"; formatPrice(3);import { CartItem } from "./product";export { Product } from "./product";export type { Product } from "./product";
verbatimModuleSyntax is on, and each line sits in a file beside product.ts from the overview. Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
- "import type loads the file, then skips the types." No. The whole line is removed, so nothing is loaded for it when the code runs.
- "A type is exported in the JavaScript, like a function." No. Types are removed, so product.js exports only
formatPrice. That's why a re-exported type can break in the browser. - "Every import needs the word type." No. Only names that are types need it. A function, a constant, or a class you call with
newneeds a plain import, or the code can't use it. - "No error from tsc means every build tool will make working JavaScript." No. tsc sees every file; a tool that sees one file at a time may keep a type it can't recognize.
verbatimModuleSyntaxmakes you mark every type, so every tool gets it right. - "verbatimModuleSyntax changes how my types are checked." No. It changes which import lines stay in the JavaScript, and asks you to mark types so that it stays safe.
- "Every .ts file is a module." No. By default, a file needs a top-level
importorexport. Without one it's a script, and its names are global, unless your tsconfig setsmoduleDetection: "force".
Practice exercises
Most exercises are files beside product.ts from the overview, and exercise 6 sits beside shopping-cart.ts. Press Edit & run on the starter code: TypeScript checks it with those files as you type, and it runs with them. Try a guess first, then use the hints.
This cart.ts imports a type and a function. Which line will TypeScript mark?
import type { CartItem, formatPrice } from "./product";
const item: CartItem = { product: { name: "Tea", price: 3 }, quantity: 2 };
console.log(formatPrice(item.product.price * item.quantity));import { type CartItem, formatPrice } from "./product"; const item: CartItem = { product: { name: "Tea", price: 3 }, quantity: 2 };console.log(formatPrice(item.product.price * item.quantity));Line 4 calls formatPrice, which came in through import type, so TypeScript reports 'formatPrice' cannot be used as a value because it was imported using 'import type' there. The fix marks only CartItem as a type: import { type CartItem, formatPrice }. Then it prints $6.00.
When the types are removed from this file, what does line 1 become? Write the whole line, or just what's left inside the braces.
import { CartItem, formatPrice } from "./product"; const item: CartItem = { product: { name: "Cake", price: 4 }, quantity: 3 };console.log(formatPrice(item.product.price * item.quantity));import { formatPrice } from "./product"; const item = { product: { name: "Cake", price: 4 }, quantity: 3 };console.log(formatPrice(item.product.price * item.quantity));CartItem is only used as a type on line 3, so it's dropped from the import, and : CartItem is removed from line 3. formatPrice is a value, so line 1 becomes import { formatPrice } from "./product";. Both versions print $12.00.
This project turns verbatimModuleSyntax on, and line 1 has two errors. Fix line 1. What does the code print?
import { CartItem, Product, formatPrice } from "./product"; const tea: Product = { name: "Tea", price: 3 };const item: CartItem = { product: tea, quantity: 2 };console.log(`${tea.name} x${item.quantity}: ${formatPrice(tea.price * item.quantity)}`);import { type CartItem, type Product, formatPrice } from "./product"; const tea: Product = { name: "Tea", price: 3 };const item: CartItem = { product: tea, quantity: 2 };console.log(`${tea.name} x${item.quantity}: ${formatPrice(tea.price * item.quantity)}`);CartItem and Product are types, so each gets type in front of it. formatPrice is a function the code calls, so it stays a plain import. The code prints Tea x2: $6.00.
This file uses TypeScript's default moduleDetection, and lines 1 and 2 both get Cannot redeclare block-scoped variable. Add one line to fix both. Which line?
const name = "Asha";const status = "signed in";console.log(`${name} is ${status}`);const name = "Asha";const status = "signed in";console.log(`${name} is ${status}`); export {};export {}; makes the file a module, so name and status belong to it and no longer clash with the browser's window.name and window.status. It prints Asha is signed in.
shop.ts passes on what product.ts exports, and adds a shop name. With verbatimModuleSyntax on, line 1 has two errors. Fix it. Then: which names does the JavaScript for shop.ts still export?
export { CartItem, Product, formatPrice } from "./product";export const shopName = "Corner Shop";export { type CartItem, type Product, formatPrice } from "./product";export const shopName = "Corner Shop";With type in front of CartItem and Product, both are removed from the JavaScript, and line 1 becomes export { formatPrice } from "./product";. Line 2 stays, so shop.js exports formatPrice and shopName.
This file imports the ShoppingCart class from shopping-cart.ts with import type, and line 3 gets an error. Fix line 1. What does the fixed code print?
import type { ShoppingCart } from "./shopping-cart"; const cart: ShoppingCart = new ShoppingCart();cart.add("Tea");console.log(cart.items.length);import { ShoppingCart } from "./shopping-cart"; const cart: ShoppingCart = new ShoppingCart();cart.add("Tea");console.log(cart.items.length);A class is both a type and a value. new ShoppingCart() needs the value, so line 1 must be a plain import. Then line 4 adds tea, and line 5 prints 1, the number of items.
Check your understanding
For each question, ask two things: is this name a type or a value, and what is left of the line once the types are removed? The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7Which import line is removed completely from the JavaScript?
Choose an answer to see the explanation.
Question 2 of 7What does TypeScript say about line 4 of this cart.ts?
Read the code, then predictimport type { Product, formatPrice } from "./product"; const cake: Product = { name: "Cake", price: 4 }; console.log(formatPrice(cake.price));Choose an answer to see the explanation.
Question 3 of 7With verbatimModuleSyntax on, which line of this file gets an error?
Read the code, then predictimport type { Product } from "./product"; import { CartItem } from "./product"; import { formatPrice } from "./product"; const tea: Product = { name: "Tea", price: 3 }; const item: CartItem = { product: tea, quantity: 1 }; console.log(formatPrice(item.product.price));Choose an answer to see the explanation.
Question 4 of 7With TypeScript's default settings, is this file a module or a script?
Read the code, then predictconst greeting = "Welcome back"; console.log(greeting);Choose an answer to see the explanation.
Question 5 of 7What does adding
export {};to the end of a file do?Choose an answer to see the explanation.
Question 6 of 7This types.ts holds only types. What does the browser get from it?
Read the code, then predictexport type Product = { name: string; price: number }; export interface CartItem { product: Product; quantity: number; }Choose an answer to see the explanation.
Question 7 of 7Why can
export { Product } from "./product";break in a Vite build when tsc reports no error?Choose an answer to see the explanation.
Key takeaways
- A file with a top-level
importorexportis a module. Its names stay inside it unless it exports them. - Export a type alias or an interface with
export, and import it by name, just like a value. - Types are removed from the JavaScript, so a type name can't be loaded from another file when the code runs.
import typebrings only types and is removed whole.import { type X, y }marks one name.- Tools that see one file at a time can't tell a type from a value.
verbatimModuleSyntaxmakes you mark every type, so the JavaScript is exactly what you wrote. - A file with no
importorexportis a script, and its names are global.export {};makes it a module.
Remember the one-liner.
Types travel between modules for checking only, so mark them with type, and only values reach the browser.
Coming next: Declaration files (.d.ts). It shows how TypeScript knows the types of JavaScript libraries that have no types of their own.