From Zero to AI

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:

  1. ./math.ts
  2. ./math.tsx
  3. ./math.d.ts
  4. ./math/index.ts
  5. ./math/index.tsx
  6. ./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

  1. Modules provide scope: Each file is its own module with private scope
  2. Named exports use curly braces: export { name } and import { name }
  3. Default exports have no braces: export default and import Name
  4. Use as to rename: Avoid naming conflicts with aliases
  5. Type-only imports optimize builds: Use import type for types
  6. Prefer named exports: They are more explicit and refactor-friendly
  7. Use relative paths: ./file for 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.

Continue to Lesson 6.2: Project Structure