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
| Feature | Numeric Enum | String Enum | Const Enum | as const Object |
|---|---|---|---|---|
| Reverse mapping | Yes | No | N/A (inlined) | No |
| Runtime object emitted | Yes | Yes | No | Yes |
isolatedModules safe | Yes | Yes | No | Yes |
| Computed values | Limited | No | No | N/A |
| Tree-shakeable | No | No | Yes (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
| Strategy | Description |
|---|---|
classic | Original TypeScript resolution; relative to importing file |
node | Mimics Node.js resolution; checks node_modules``package.json types/typings field |
node16 / nodenext | Node.js ESM-aware resolution (TypeScript 4.7+) |
bundler | Resolution 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:
- Import directly from the source file rather than the barrel when only one or two names are needed.
- Use named exports (not
export *). - Ensure the bundler supports side-effect-free tree-shaking (mark the package as
"sideEffects": falseinpackage.json).
\{
"name": "my-lib",
"sideEffects": false
\}
Recommended Project Structure
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:
- Install the
@types/Xpackage. - Create a declaration file:
declare module "X";. - 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