Lesson 6.1: Import and Export
Duration: 55 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Export values from a module using named and default exports
- Import values into another module
- Understand when to use named vs default exports
- Rename imports and exports
- Use type-only imports for better performance
What Are Modules?
A module is simply a file that exports code for other files to use. Before modules, all JavaScript code shared the same global scope, leading to naming conflicts and messy dependencies.
// Without modules: everything is global
var userName = 'Alice'; // Could conflict with other code!
function greet() {} // Could be overwritten!
With modules, each file has its own scope. You explicitly choose what to share:
// With modules: explicit sharing
// user.ts
export const userName = 'Alice'; // Only available if imported
export function greet() {} // Must be imported to use
Named Exports
Named exports let you export multiple values from a file. Each export has a specific name:
// math.ts
export const PI = 3.14159;
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export interface MathResult {
value: number;
operation: string;
}
You can also declare first and export later:
// math.ts
const PI = 3.14159;
function add(a: number, b: number): number {
return a + b;
}
function subtract(a: number, b: number): number {
return a - b;
}
interface MathResult {
value: number;
operation: string;
}
// Export at the end
export { PI, add, subtract, MathResult };
Importing Named Exports
Use curly braces to import named exports:
// main.ts
import { PI, add, subtract } from './math';
console.log(PI); // 3.14159
console.log(add(2, 3)); // 5
console.log(subtract(5, 2)); // 3
Import only what you need:
// main.ts
import { add } from './math';
// Only import add
console.log(add(2, 3)); // 5
// PI is not available here
Renaming Imports
Use as to rename imports and avoid naming conflicts:
// main.ts
import { subtract as minus, add as sum } from './math';
console.log(sum(2, 3)); // 5
console.log(minus(5, 2)); // 3
This is useful when two modules export the same name:
// main.ts
import { format as formatCurrency } from './currency-utils';
import { format as formatDate } from './date-utils';
formatDate(new Date());
formatCurrency(99.99);
Renaming Exports
You can also rename values when exporting:
// internal.ts
function internalAdd(a: number, b: number): number {
return a + b;
}
// Export with a different name
export { internalAdd as add };
// main.ts
import { add } from './internal';
console.log(add(2, 3)); // 5
Default Exports
Each module can have one default export. This is the "main" thing the module provides:
// calculator.ts
export default class Calculator {
add(a: number, b: number): number {
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
multiply(a: number, b: number): number {
return a * b;
}
divide(a: number, b: number): number {
if (b === 0) throw new Error('Cannot divide by zero');
return a / b;
}
}
Import default exports without curly braces:
// main.ts
import Calculator from './calculator';
const calc = new Calculator();
console.log(calc.add(2, 3)); // 5
You can name default imports anything you want:
// main.ts
import Calc from './calculator';
// Different name, same class
import MyCalculator from './calculator';
// Also works
const calc = new Calc();
Mixing Named and Default Exports
A module can have both default and named exports:
// logger.ts
export default class Logger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
error(message: string): void {
console.error(`[ERROR] ${message}`);
}
}
export enum LogLevel {
Debug = 'debug',
Info = 'info',
Warning = 'warning',
Error = 'error',
}
export interface LogEntry {
level: LogLevel;
message: string;
timestamp: Date;
}
Import them together:
// main.ts
import Logger, { LogEntry, LogLevel } from './logger';
const logger = new Logger();
logger.log('Application started');
const entry: LogEntry = {
level: LogLevel.Info,
message: 'User logged in',
timestamp: new Date(),
};
Import Everything with Namespace
Use * as to import all exports as a namespace object:
// math.ts
export const PI = 3.14159;
export const E = 2.71828;
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
// main.ts
import * as MathUtils from './math';
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.E); // 2.71828
console.log(MathUtils.add(2, 3)); // 5
console.log(MathUtils.multiply(2, 3)); // 6
This is useful when a module has many related exports.
Type-Only Imports
When importing only types (interfaces, type aliases), use import type for better performance:
// types.ts
export interface User {
id: number;
name: string;
email: string;
}
export type UserRole = 'admin' | 'user' | 'guest';
export interface Product {
id: number;
name: string;
price: number;
}
// main.ts
import type { Product, User, UserRole } from './types';
// These are only used as types
function greetUser(user: User): string {
return `Hello, ${user.name}!`;
}
function checkRole(role: UserRole): boolean {
return role === 'admin';
}
Type-only imports are erased during compilation, reducing bundle size.
You can also mark individual imports as type-only:
// user-service.ts
import { type User, createUser } from './user';
function processUser(user: User): void {
// User is type-only, createUser is a value
}
Re-exporting
You can re-export from another module:
// utils/string-utils.ts
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function lowercase(str: string): string {
return str.toLowerCase();
}
// utils/index.ts
// Re-export everything from string-utils
export * from './string-utils';
// Re-export specific items
export { capitalize } from './string-utils';
// Re-export with rename
export { capitalize as cap } from './string-utils';
Module Resolution
TypeScript needs to find the files you import. The path you use matters:
// Relative imports - start with ./ or ../
import { useState } from 'react';
// Subfolder
// Non-relative imports - packages from node_modules
import express from 'express';
// Same folder
import { User } from '../models/user';
// Parent folder
import { config } from './config/app';
import { add } from './math';
Always use relative paths for your own files and non-relative paths for external packages.
File Extensions
In TypeScript, you typically omit file extensions:
// TypeScript resolves these automatically
import { add } from './math';
// Finds math.ts or math/index.ts
import { User } from './models';
// Finds models/index.ts
TypeScript looks for files in this order:
./math.ts./math.tsx./math.d.ts./math/index.ts./math/index.tsx./math/index.d.ts
Named vs Default Exports: Guidelines
Prefer named exports when:
- A module exports multiple related items
- You want explicit, searchable names
- You want to prevent renaming (consistency)
// Good: multiple related exports
export function validateEmail(email: string): boolean {}
export function validatePhone(phone: string): boolean {}
export function validateAge(age: number): boolean {}
Consider default exports when:
- A module has one main export
- The export is a class or component
- You want flexibility in naming
// Good: one main class
export default class UserService {
// ...
}
Many style guides prefer named exports because:
- Easier to refactor (IDE can find all usages)
- More explicit (you know exactly what you import)
- Better tree-shaking in some bundlers
Common Mistakes
Mistake 1: Forgetting Curly Braces
// main.ts
// Wrong: add is a named export
import add from './math';
// Error!
// Correct: use curly braces
import { add } from './math';
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
Mistake 2: Wrong Path
// File structure:
// src/
// utils/
// math.ts
// main.ts
// main.ts
// Wrong: missing folder
import { add } from "./math"; // Error: cannot find module
// Correct: include folder
import { add } from "./utils/math";
Mistake 3: Circular Dependencies
// a.ts
import { b } from "./b";
export const a = "A" + b;
// b.ts
import { a } from "./a"; // Circular!
export const b = "B" + a;
Avoid circular dependencies by restructuring your code or creating a third module.
Exercises
Exercise 1: Basic Exports
Create a file string-utils.ts with these exported functions:
// Your code in string-utils.ts
// Test in main.ts
import { countWords, reverse, truncate } from './string-utils';
console.log(reverse('hello')); // "olleh"
console.log(countWords('Hello World')); // 2
console.log(truncate('Hello World', 5)); // "Hello..."
Solution
// string-utils.ts
export function reverse(str: string): string {
return str.split('').reverse().join('');
}
export function countWords(str: string): number {
return str
.trim()
.split(/\s+/)
.filter((word) => word.length > 0).length;
}
export function truncate(str: string, maxLength: number): string {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength) + '...';
}
Exercise 2: Default Export Class
Create a file counter.ts with a Counter class as default export:
// Your code in counter.ts
// Test in main.ts
import Counter from './counter';
const counter = new Counter(10);
console.log(counter.getValue()); // 10
counter.increment();
console.log(counter.getValue()); // 11
counter.decrement();
counter.decrement();
console.log(counter.getValue()); // 9
counter.reset();
console.log(counter.getValue()); // 10
Solution
// counter.ts
export default class Counter {
private value: number;
private initialValue: number;
constructor(initial: number = 0) {
this.value = initial;
this.initialValue = initial;
}
getValue(): number {
return this.value;
}
increment(): void {
this.value++;
}
decrement(): void {
this.value--;
}
reset(): void {
this.value = this.initialValue;
}
}
Exercise 3: Mixed Exports
Create a file user.ts with a default User class and named helper functions:
// Your code in user.ts
// Test in main.ts
import User, { createUser, isAdult } from './user';
const user1 = new User('Alice', 25, 'alice@example.com');
const user2 = createUser('Bob', 17, 'bob@example.com');
console.log(user1.getInfo()); // "Alice (25) - alice@example.com"
console.log(isAdult(user1)); // true
console.log(isAdult(user2)); // false
Solution
// user.ts
export default class User {
constructor(
public name: string,
public age: number,
public email: string
) {}
getInfo(): string {
return `${this.name} (${this.age}) - ${this.email}`;
}
}
export function createUser(name: string, age: number, email: string): User {
return new User(name, age, email);
}
export function isAdult(user: User): boolean {
return user.age >= 18;
}
Exercise 4: Type-Only Imports
Create a types file and use type-only imports:
// types.ts - Create types for a simple e-commerce system
// cart.ts - Import types and create a cart class
// Test in main.ts
import Cart from './cart';
import type { CartItem, Product } from './types';
const cart = new Cart();
cart.addItem({ id: 1, name: 'Book', price: 29.99 }, 2);
cart.addItem({ id: 2, name: 'Pen', price: 4.99 }, 5);
console.log(cart.getTotal()); // 84.93
console.log(cart.getItems()); // Array of CartItems
Solution
// cart.ts
import type { CartItem, Product } from './types';
// types.ts
export interface Product {
id: number;
name: string;
price: number;
}
export interface CartItem {
product: Product;
quantity: number;
}
export default class Cart {
private items: CartItem[] = [];
addItem(product: Product, quantity: number): void {
const existingItem = this.items.find((item) => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({ product, quantity });
}
}
getItems(): CartItem[] {
return [...this.items];
}
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.product.price * item.quantity, 0);
}
}
Key Takeaways
- Modules provide scope: Each file is its own module with private scope
- Named exports use curly braces:
export { name }andimport { name } - Default exports have no braces:
export defaultandimport Name - Use
asto rename: Avoid naming conflicts with aliases - Type-only imports optimize builds: Use
import typefor types - Prefer named exports: They are more explicit and refactor-friendly
- Use relative paths:
./filefor your code, package names for dependencies
Resources
| Resource | Type | Description |
|---|---|---|
| TypeScript Handbook: Modules | Documentation | Official module guide |
| MDN: JavaScript Modules | Documentation | ES modules in JavaScript |
| TypeScript Module Resolution | Documentation | How TypeScript finds modules |
Next Lesson
Now that you know how to import and export, let us learn how to structure your project folders.