Lesson 07-Modular Design with TypeScript

TypeScript Module System

TypeScript Module Basics

TypeScript Module Types:

  1. ES Modules (ESM):
    • Uses import/export syntax.
    • Standard module system for modern frontend development.
    • Supports static analysis and Tree Shaking.
  2. CommonJS (CJS):
    • Uses require/module.exports syntax.
    • Traditional module system for Node.js.
    • Supports dynamic loading.
  3. AMD (Asynchronous Module Definition):
    • Uses define/require syntax.
    • Primarily for asynchronous loading in browsers.
    • Implemented via RequireJS.

Module Declaration Examples:

// ES Module example
export interface User {
  id: number;
  name: string;
}

export function getUser(id: number): Promise<User> {
  // Implementation
}

// CommonJS example
interface Product {
  id: number;
  name: string;
  price: number;
}

function getProduct(id: number): Product {
  // Implementation
}

module.exports = {
  getProduct
};

Module File Extensions:

  • .ts: TypeScript source files.
  • .tsx: TypeScript JSX files.
  • .d.ts: TypeScript declaration files.

Module Import and Export Syntax

ES Module Import/Export:

// Named exports
export const PI = 3.14159;
export function circleArea(radius: number): number {
  return PI * radius * radius;
}

// Default export
export default class Circle {
  constructor(public radius: number) {}

  area(): number {
    return PI * this.radius * this.radius;
  }
}

// Re-export
export { PI } from './constants';
export { default as Circle } from './circle';

CommonJS Import/Export:

// Export
const utils = {
  add: (a: number, b: number) => a + b,
  subtract: (a: number, b: number) => a - b
};

module.exports = utils;
// Or
exports.add = (a: number, b: number) => a + b;
exports.subtract = (a: number, b: number) => a - b;

// Import
import utils = require('./utils');
// Or (preferred in TypeScript with ESM syntax)
import * as utils from './utils';

Dynamic Import:

// ES Module dynamic import
const module = await import('./dynamic-module');

// Dynamic import with type assertion
interface DynamicModule {
  default: { init: () => void };
  helper: () => string;
}

const { default: main, helper } = await import('./dynamic-module') as DynamicModule;
main.init();
console.log(helper());

Module Resolution Strategies

TypeScript Module Resolution Rules:

  1. Classic Resolution:
    • Similar to Node.js require resolution.
    • Supports node_modules lookup.
  2. Node Resolution:
    • More precise simulation of Node.js module resolution algorithm.
    • Supports package.json main field.

Configuring Module Resolution Strategy:

// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "node", // or "classic"
    "baseUrl": "./",           // Base path for resolution
    "paths": {                 // Path mappings
      "@/*": ["src/*"]
    },
    "rootDirs": ["src", "types"] // Multiple root directories
  }
}

Path Mapping Example:

// Import using path mapping
import { Button } from '@/components/Button';
// Resolves to src/components/Button

// tsconfig.json configuration
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "components/*": ["src/components/*"]
    }
  }
}

TypeScript and Modular Toolchains

TypeScript Integration with Webpack

Webpack TypeScript Configuration:

// webpack.config.js
const path = require('path');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
  entry: './src/index.ts',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js']
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  plugins: [
    new ForkTsCheckerWebpackPlugin() // Type checking in a separate process
  ]
};

Advanced Webpack + TypeScript Configuration:

// webpack.config.js
module.exports = {
  // ...other configurations
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [
          {
            loader: 'ts-loader',
            options: {
              transpileOnly: true, // Transpile only, skip type checking
              happyPackMode: true, // Compatible with HappyPack
              experimentalWatchApi: true // Experimental watch API
            }
          }
        ],
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    },
    extensions: ['.ts', '.tsx', '.js', '.json']
  }
};

TypeScript Integration with Rollup

Rollup TypeScript Configuration:

// rollup.config.js
import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
  input: 'src/index.ts',
  output: {
    file: 'dist/bundle.js',
    format: 'esm'
  },
  plugins: [
    resolve(), // Resolve modules from node_modules
    commonjs(), // Convert CommonJS modules to ES6
    typescript({
      tsconfig: './tsconfig.json',
      declaration: true, // Generate declaration files
      declarationDir: 'dist/types' // Declaration file output directory
    })
  ]
};

Advanced Rollup + TypeScript Configuration:

// rollup.config.js
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
import dts from 'rollup-plugin-dts';

// Main configuration (for generating JS)
const config = {
  input: 'src/index.ts',
  output: [
    {
      file: 'dist/bundle.cjs.js',
      format: 'cjs'
    },
    {
      file: 'dist/bundle.esm.js',
      format: 'esm'
    }
  ],
  plugins: [
    typescript({
      tsconfig: './tsconfig.json',
      exclude: ['**/*.test.ts']
    })
  ]
};

// Declaration file configuration (separate generation)
const dtsConfig = {
  input: 'src/index.ts',
  output: [{ file: 'dist/types/index.d.ts', format: 'esm' }],
  plugins: [dts()]
};

export default [config, dtsConfig];

TypeScript Integration with Vite

Vite TypeScript Configuration:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': '/src'
    }
  },
  server: {
    port: 3000
  },
  // TypeScript configuration is typically managed via tsconfig.json
  // Vite has built-in TypeScript support, no additional plugins required
});

Advanced Vite + TypeScript Features:

  1. Environment Variable Type Declarations:
// src/env.d.ts
interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_APP_TITLE: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}
  1. Path Alias Type Support:
// src/path-aliases.d.ts
/// <reference types="vite/client" />

declare module '@/components/*';
declare module '@/utils/*';

TypeScript Modular Best Practices

Module Design and Organization

Module Design Principles:

  1. Single Responsibility: Each module should do one thing.
  2. Clear Interfaces: Clearly define inputs and outputs.
  3. Minimal Exposure: Expose only what is necessary.
  4. Explicit Dependencies: Explicitly declare all dependencies.

Module Organization Structure Example:

src/
├── components/       # UI components
   ├── Button/
      ├── index.ts  # Main export
      ├── Button.tsx
      ├── Button.css
      └── types.ts  # Component-specific types
   └── ...
├── hooks/            # Custom Hooks
   ├── useFetch.ts
   └── ...
├── services/         # API services
   ├── api.ts
   └── user-service.ts
├── utils/            # Utility functions
   ├── helpers.ts
   └── ...
├── types/            # Global type definitions
   ├── user.ts
   └── ...
└── index.ts          # Application entry

Module Export Patterns:

// Recommended: Clear single export file
// components/Button/index.ts
export { default } from './Button';
export type { ButtonProps } from './types';

// Not recommended: Direct export of multiple contents (may cause naming conflicts)
// components/Button/index.ts
export { default as Button } from './Button';
export { ButtonProps } from './types';

Type-Safe Modules

Module Type Definitions:

// Define module interface
interface LoggerModule {
  log: (message: string) => void;
  error: (message: string) => void;
  setLevel: (level: 'debug' | 'info' | 'warn' | 'error') => void;
}

// Implement module
const logger: LoggerModule = {
  log: (message) => console.log(message),
  error: (message) => console.error(message),
  setLevel: (level) => { /* ... */ }
};

// Export module
export default logger;

// Use module (type-safe)
import logger from './logger';
logger.log('Hello'); // Correct
logger.log(123); // Type error

Generic Module Example:

// Define generic storage module
interface StorageModule<T> {
  get(key: string): T | undefined;
  set(key: string, value: T): void;
  remove(key: string): void;
}

// Implement specific storage module
class LocalStorageModule<T> implements StorageModule<T> {
  get(key: string): T | undefined {
    const item = localStorage.getItem(key);
    return item ? JSON.parse(item) : undefined;
  }

  set(key: string, value: T): void {
    localStorage.setItem(key, JSON.stringify(value));
  }

  remove(key: string): void {
    localStorage.removeItem(key);
  }
}

// Use generic module
const userStorage = new LocalStorageModule<User>();
userStorage.set('current-user', { id: 1, name: 'Alice' });
const user = userStorage.get('current-user'); // Type is User | undefined

Module Version Control and Compatibility

Semantic Versioning (SemVer) with TypeScript:

  1. Major Version:
    • Breaking API changes.
    • May require significant updates to TypeScript type definitions.
  2. Minor Version:
    • Backward-compatible feature additions.
    • May add new types or optional properties.
  3. Patch Version:
    • Backward-compatible bug fixes.
    • May fix errors in type definitions.

Version Compatibility Strategies:

// package.json
{
  "name": "my-library",
  "version": "1.2.3",
  "types": "dist/types/index.d.ts", // Type declaration file location
  "type": "module", // or "commonjs"
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",  // ES module entry
      "require": "./dist/cjs/index.js"  // CommonJS entry
    },
    "./types": {
      "import": "./dist/types/index.d.ts",
      "require": "./dist/types/index.d.ts"
    }
  },
  "peerDependencies": {
    "react": ">=16.8.0 <18.0.0" // React version compatibility requirements
  }
}

Type Compatibility Checks:

// Check module type compatibility
import { SomeType } from 'some-library';

// Use type assertion to check compatibility
function isCompatible(value: unknown): value is SomeType {
  return (
    typeof value === 'object' &&
    value !== null &&
    'requiredProperty' in value &&
    typeof (value as SomeType).requiredProperty === 'string'
  );
}

// Runtime type checking libraries (e.g., io-ts, zod) can be used with TypeScript
import * as t from 'io-ts';
import { either } from 'fp-ts/Either';

const User = t.type({
  id: t.number,
  name: t.string
});

type User = t.TypeOf<typeof User>; // Generate corresponding TypeScript type

function validateUser(input: unknown): either<t.Errors, User> {
  return User.decode(input);
}

Advanced TypeScript Modular Topics

Dynamic Module Loading with Type Safety

Type-Safe Dynamic Imports:

// Define dynamic module types
interface DynamicModule {
  default: {
    init: () => void;
    version: string;
  };
  helper: (input: string) => number;
}

// Dynamic import with type assertion
async function loadDynamicModule(): Promise<DynamicModule> {
  const module = await import('./dynamic-module');

  // Optional runtime check
  if (!module.default || typeof module.default.init !== 'function') {
    throw new Error('Invalid module structure');
  }

  // Type assertion
  return module as DynamicModule;
}

// Use dynamic module
const { default: main, helper } = await loadDynamicModule();
main.init();
console.log(helper('test'));

Dynamic Imports with React Components:

// Define type for dynamically loaded components
type LazyComponent = React.LazyExoticComponent<React.ComponentType<any>>;

// Dynamic component loading function
function lazyLoadComponent<T extends React.ComponentType<any>>(
  importFn: () => Promise<{ default: T }>
): LazyComponent {
  return React.lazy(importFn);
}

// Usage example
const LazyButton = lazyLoadComponent(() => 
  import('./components/Button') as Promise<{ default: React.ComponentType<{ text: string }> }>
);

// Use in React component
function App() {
  return (
    <React.Suspense fallback={<div>Loading...</div>}>
      <LazyButton text="Click me" />
    </React.Suspense>
  );
}

Module Federation with TypeScript

TypeScript with Webpack Module Federation:

// Define remote module types (in consumer)
// src/remote-types.d.ts
declare module 'remote-app/Button' {
  import { ComponentType } from 'react';
  const Button: ComponentType<{ text: string }>;
  export default Button;
}

// Use remote module
import RemoteButton from 'remote-app/Button';

function App() {
  return <RemoteButton text="Remote Button" />;
}

Shared Type Configuration:

// Sharing types in Module Federation
// Option 1: Use a separate type package
// Create a types package containing all shared types
// Other applications depend on this types package

// Option 2: Use declaration merging
// Declare remote module types in consumer (as above)

// Option 3: Use API extraction tools
// Extract type definitions from remote modules using tools

Type Sharing in Micro-Frontends:

// Main application defines shared types
// shared-types.d.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

export interface AuthToken {
  token: string;
  expiresAt: Date;
}

// Reference shared types in micro-apps
// Ensure type definitions are synchronized
// Can be shared via npm package or declaration files

Modular Testing Strategies

Module Unit Testing:

// Test utility module
// math-utils.test.ts
import { add, multiply } from './math-utils';

describe('math-utils', () => {
  it('should add two numbers correctly', () => {
    expect(add(2, 3)).toBe(5);
  });

  it('should multiply two numbers correctly', () => {
    expect(multiply(2, 3)).toBe(6);
  });
});

// Test React component
// Button.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import Button from './Button';

describe('Button', () => {
  it('should render with given text', () => {
    render(<Button text="Click me" />);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('should call onClick when clicked', () => {
    const onClick = jest.fn();
    render(<Button text="Click me" onClick={onClick} />);
    screen.getByText('Click me').click();
    expect(onClick).toHaveBeenCalledTimes(1);
  });
});

Module Integration Testing:

// Test module integration
// user-service.test.ts
import UserService from './user-service';
import api from './api';

jest.mock('./api');

describe('UserService', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should fetch user data correctly', async () => {
    const mockUser = { id: 1, name: 'Alice' };
    (api.get as jest.Mock).mockResolvedValue({ data: mockUser });

    const userService = new UserService();
    const user = await userService.getUser(1);

    expect(api.get).toHaveBeenCalledWith('/users/1');
    expect(user).toEqual(mockUser);
  });
});

// Test React component and Hook integration
// useFetch.test.ts
import { renderHook, act } from '@testing-library/react-hooks';
import useFetch from './useFetch';

describe('useFetch', () => {
  it('should fetch data on mount', async () => {
    const mockData = { message: 'Hello' };
    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve(mockData),
      })
    ) as jest.Mock;

    const { result, waitForNextUpdate } = renderHook(() => useFetch('/test'));

    expect(result.current.loading).toBe(true);
    await waitForNextUpdate();

    expect(result.current.loading).toBe(false);
    expect(result.current.data).toEqual(mockData);
  });
});

Summary

TypeScript brings robust type safety to modular development, enabling developers to leverage the organizational benefits of modularity while benefiting from compile-time type checking. By properly configuring TypeScript integration with various modular toolchains (Webpack, Rollup, Vite, etc.), developers can build modern frontend applications that are both efficient and reliable.

Share your love