Skip to content

Enums and Modules

Numeric Enums

Numeric enums assign auto-incrementing integer values to their members. By default, the first member Has value 0And each subsequent member increments by 1.

enum Direction \{
  Up,
  Down,
  Left,
  Right,
\}

const d: Direction = Direction.Up;
console.log(Direction.Up);
console.log(Direction[0]);

Numeric enums support reverse mapping: given a numeric value, the enum name can be retrieved. This is a runtime feature generated by the TypeScript compiler.

Explicit Values

Members can be assigned explicit values:

enum HttpStatus \{
  OK = 200,
  NotFound = 404,
  InternalError = 500,
  ServiceUnavailable = 503,
\}

console.log(HttpStatus.OK);
console.log(HttpStatus["200']);

Computed Values

Enum members can have computed values, but only when all preceding members have literal values (constant or numeric literals):

enum FileAccess \{
  None,
  Read = 1 << 0,
  Write = 1 << 1,
  ReadWrite = Read | Write,
\}

const perms = FileAccess.ReadWrite;
console.log(perms & FileAccess.Read);
console.log(perms & FileAccess.Write);

Computed enum members do not support reverse mapping because the compiler cannot determine the name At compile time.

String Enums

String enums assign string literal values to their members. They do not support auto-incrementing or Reverse mapping.

enum LogLevel \{
  Error = 'ERROR',
  Warn = 'WARN',
  Info = 'INFO',
  Debug = 'DEBUG',
\}

function log(level: LogLevel, message: string): void \{
  console.log(`[${level}] ${message}`);
\}

log(LogLevel.Error, 'Something went wrong');

String enums are preferred over numeric enums in most cases because they produce more readable Runtime values and are safer for serialisation (e.g., JSON).

Const Enums

Const enums are inlined by the compiler. They are completely erased during compilation and replaced With their literal values at each use site.

const enum Direction \{
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
\}

const dir: Direction = Direction.Up;

The compiled JavaScript output is:

const dir = 'UP';

No JavaScript Direction object is emitted. This makes const enums suitable for Performance-critical code.

Common Pitfall: Const enums cannot be used across module boundaries when isolatedModules is Enabled (required by bundlers like esbuild, swc, and Babel). In those environments, use regular Enums or as const objects instead.

Const Enum Alternatives

When isolatedModules prevents const enum usage, use an as const object:

const Direction = \{
  Up: "UP'',
  Down: "DOWN',
  Left: "LEFT'',
  Right: "RIGHT',
\} as const;

type Direction = (typeof Direction)[keyof typeof Direction];

function move(dir: Direction): void \{
  console.log(dir);
\}

move(Direction.Up);

Heterogenous Enums

Heterogenous enums mix string and numeric members. This pattern is generally discouraged because it Reduces type safety and readability:

enum Mixed \{
  No = 0,
  Yes = 'YES',
\}

Prefer discriminated unions (see below) over heterogenous enums.

Union Enum Pattern

The union enum pattern uses string enums to create a type-safe, self-documenting set of values That integrates with TypeScript’s narrowing system:

enum ShapeKind \{
  Circle = 'circle',
  Square = 'square',
  Triangle = 'triangle',
\}

interface Circle \{
  kind: ShapeKind.Circle;
  radius: number;
\}

interface Square \{
  kind: ShapeKind.Square;
  side: number;
\}

interface Triangle \{
  kind: ShapeKind.Triangle;
  base: number;
  height: number;
\}

type Shape = Circle | Square | Triangle;

function area(shape: Shape): number \{
  switch (shape.kind) \{
    case ShapeKind.Circle:
      return Math.PI * shape.radius ** 2;
    case ShapeKind.Square:
      return shape.side ** 2;
    case ShapeKind.Triangle:
      return (shape.base * shape.height) / 2;
  \}
\}

The compiler verifies that the switch is exhaustive (all enum members are handled) because shape.kind is a union of the enum’s literal types.

Union of Literal Types (Without Enum)

The same pattern can be achieved without an enum declaration:

type ShapeKind = 'circle' | 'square' | 'triangle';

interface Circle \{
  kind: "circle'';
  radius: number;
\}

type Shape = Circle | Square | Triangle;

This approach is sometimes preferred because it avoids the runtime overhead of enum objects and is Compatible with isolatedModules.

Enum Comparison Table

FeatureNumeric EnumString EnumConst Enumas const Object
Reverse mappingYesNoN/A (inlined)No
Runtime object emittedYesYesNoYes
isolatedModules safeYesYesNoYes
Computed valuesLimitedNoNoN/A
Tree-shakeableNoNoYes (inlined)Yes

ES Modules: Import and Export

Named Exports

export function add(a: number, b: number): number {
  return a + b;
\}

export interface Vector {
  x: number;
  y: number;
\}

export const PI = Math.PI;

Named Imports

import { add, Vector, PI } from "@/data/flashcards/typescript/math.js';

Default Exports

export default class Calculator {
  add(a: number, b: number): number \{
    return a + b;
  \}
\}
import Calculator from './calculator';

Renaming Exports and Imports

export { add as sum } from '@/data/flashcards/typescript/math.js';

import { Vector as Vec2D } from '@/data/flashcards/typescript/math.js';

Re-exports

Re-exports forward exports from another module:

export { add, subtract } from '@/data/flashcards/typescript/math.js';

export * from './utils';

Re-exporting with renaming:

export { add as plus, subtract as minus } from '@/data/flashcards/typescript/math.js';

Type-Only Imports and Exports

TypeScript 3.8+ supports type-only imports and exports, which are erased during compilation:

import type { Vector } from '@/data/flashcards/typescript/math.js";

export type { Vector };

This is useful for avoiding circular dependencies and for ensuring that type imports do not produce Runtime side effects.

Import Assertions (TypeScript 4.5+)

import data from './data.json' assert { type: "json'' };

Module Augmentation and Declaration Merging

Declaration Merging for Interfaces

Multiple interface declarations with the same name are automatically merged:

interface Window \{
  title: string;
\}

interface Window \{
  width: number;
  height: number;
\}

const w: Window = \{
  title: "Main',
  width: 800,
  height: 600,
\};

Module Augmentation

Module augmentation extends an existing module with new declarations:

declare module 'express' \{
  interface Request \{
    userId?: string;
  \}
\}

After this declaration, all Request objects in the express module have an optional userId Property.

Global Augmentation

declare global \{
  interface Array<T> \{
    myCustomMethod(): T | undefined;
  \}
\}

Array.prototype.myCustomMethod = function () \{
  return this[0];
\};

Namespaces

Namespaces (formerly called “internal modules”) provide a way to organise code into scoped Containers. They are an older pattern that has largely been superseded by ES modules.

Declaration

namespace Geometry \{
  export interface Point {
    x: number;
    y: number;
  \}

  export function distance(a: Point, b: Point): number {
    const dx = a.x - b.x;
    const dy = a.y - b.y;
    return Math.sqrt(dx * dx + dy * dy);
  \}

  namespace Sub \{
    export function origin(): Point {
      return \{ x: 0, y: 0 \};
    \}
  \}
\}

const p1: Geometry.Point = \{ x: 1, y: 2 \};
const p2: Geometry.Sub.origin();
console.log(Geometry.distance(p1, p2));

When to Use Namespaces

Namespaces should be avoided in modern TypeScript. They exist primarily for backward compatibility And for declaring types that span multiple files (using .d.ts files). Use ES modules instead.

Common Pitfall: Do not mix namespaces and ES modules in the same file. A file that contains a Top-level import or export is an ES module, and any namespace within it is scoped to that module Rather than being global.

Triple-Slash Directives

Triple-slash directives are single-line comments that serve as compiler instructions. They must Appear at the top of the file.

/// <reference path="..."/>

Instructs the compiler to include another file in the compilation:

/// <reference path="utils.ts"/>

/// <reference types="..."/>

Instructs the compiler to include a package from @types/:

/// <reference types="node"/>

/// <reference lib="..."/>

Instructs the compiler to include a built-in lib file:

/// <reference lib="es2020"/>

In modern TypeScript projects, triple-slash directives are rarely needed because the compiler Resolves dependencies automatically through tsconfig.json and node_modules.

Module Resolution

Classic vs Node Resolution

StrategyDescription
classicOriginal TypeScript resolution; relative to importing file
nodeMimics Node.js resolution; checks node_modules``package.json types/typings field
node16 / nodenextNode.js ESM-aware resolution (TypeScript 4.7+)
bundlerResolution for bundlers that handle module resolution themselves (TypeScript 5.0+)

Path Aliases in tsconfig

\{
  "compilerOptions": \{
    "baseUrl": ".",
    "paths": \{
      "@utils/*": ["src/utils/*"],
      "@components/*": ["src/components/*"],
      "@types/*": ["src/types/*"]
    \}
  \}
\}

With these path aliases:

import { formatDate } from '@utils/date';
import { Button } from '@components/Button';

Path aliases must be mirrored in the bundler configuration (e.g., vite.config.ts webpack.config.js) to resolve at build time.

rootDirs

\{
  "compilerOptions": \{
    "rootDirs": ["src", "generated"]
  \}
\}

Multiple rootDirs entries are treated as a single virtual directory. This allows files from Different directories to be imported as if they were co-located.

Ambient Modules and Declaration Files

Declaration Files (.d.ts)

Declaration files describe the types of JavaScript modules or global variables without providing Implementations. They use declare keywords:

declare function fetch(url: string): Promise<Response>;
declare class URL \{
  constructor(url: string);
\}
declare const process: \{ env: Record<string, string | undefined> \};

Ambient Module Declarations

declare module 'my-untyped-library' \{
  export function doSomething(input: string): number;
  export interface Config {
    apiKey: string;
  \}
\}

This tells the compiler: “when someone imports from my-untyped-libraryTreat it as having this Shape.”

global.d.ts

A global.d.ts file (or any .d.ts file that is a script, not a module) declares global types:

declare global \{
  interface String \{
    capitalize(): string;
  \}
\}

String.prototype.capitalize = function () \{
  return this.charAt(0).toUpperCase() + this.slice(1);
\};

Module Declaration for Non-TypeScript Files

declare module '*.svg' \{
  const content: string;
  export default content;
\}

declare module '*.json' \{
  const value: any;
  export default value;
\}

These are called wildcard module declarations and are used to allow importing non-TypeScript Files.

Barrel Files and Tree-Shaking

Barrel Files

A barrel file (index.ts) re-exports from multiple modules:

export { add, subtract, multiply } from '@/data/flashcards/typescript/math.js';
export { formatDate, parseDate } from './date';
export { log, warn, error } from './logger';

Consumers import from the barrel:

import { add, formatDate, log } from './lib';

Tree-Shaking Implications

Barrel files can interfere with tree-shaking in certain bundlers. When a barrel file re-exports from Many modules, importing a single name may cause the bundler to include code for all re-exported Names.

Mitigation strategies:

  1. Import directly from the source file rather than the barrel when only one or two names are needed.
  2. Use named exports (not export *).
  3. Ensure the bundler supports side-effect-free tree-shaking (mark the package as "sideEffects": false in package.json).
\{
  "name": "my-lib",
  "sideEffects": false
\}
src/
  utils/
    math.ts
    date.ts
    string.ts
    index.ts
  components/
    Button.tsx
    Input.tsx
    index.ts
  index.ts

Each subdirectory has its own barrel file. The top-level index.ts re-exports from subdirectory Barrels.

Common Pitfalls

Pitfall 1: Enum Value Conflicts

Numeric enums with auto-incrementing values can silently collide with manually assigned values:

enum Status \{
  Active = 0,
  Inactive,
  Pending = 1,
\}

Here, Inactive is 1 and Pending is also 1. Use explicit values to avoid collisions.

Pitfall 2: Circular Dependencies

Module A imports from B, and B imports from A. TypeScript can handle circular type dependencies, but Runtime circular dependencies may cause undefined values at import time. Use type-only imports to Break the cycle:

import type { B } from './b';

Pitfall 3: Missing Declaration Files

Importing a JavaScript library without type definitions produces the error “Could not find Declaration file for module ‘X’”. Solutions:

  1. Install the @types/X package.
  2. Create a declaration file: declare module "X";.
  3. Set noImplicitAny: false (not recommended).

Pitfall 4: isolatedModules and Const Enums

When isolatedModules is enabled, const enums cannot be used across files because each file is Transpiled independently and the compiler cannot inline values from other files. Use as const Objects as an alternative.

Summary

This topic covers the core concepts of enums and modules, including underlying theory, practical implementation, and key applications.

Key concepts include:

  • type annotations and interfaces
  • generics and utility types
  • async/await and Promises
  • modules and namespaces
  • type guards and narrowing

Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.

Worked Examples

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.

Intuition

TypeScript enums create named constants that integrate with the type system, though union of string literals often provides a simpler alternative. Numeric enums support reverse mapping at runtime, while string enums produce more readable output. Const enums are completely inlined during compilation, eliminating runtime overhead. ES modules provide the standard import/export mechanism, with type-only imports ensuring no runtime side effects. Module augmentation extends existing type definitions without modifying source code.

Cross-References

  • [[typescript/typescript]] - TypeScript language overview
  • [[typescript/types-and-annotations]] - Literal types and union patterns
  • [[typescript/tooling-and-configuration]] - Declaration files and ambient modules
  • [[typescript/advanced-patterns]] - Discriminated union patterns as enum alternatives

See Also