
A Gentle Introduction to Enums, Generics, and Modules in TypeScript
Learn the fundamentals of enums, generics, and modules in TypeScript with simple examples, when-to-use guidance, and common pitfalls—perfect for beginners.
dogunfx
TypeScript adds helpful tools on top of JavaScript. Three of the most useful—enums, generics, and modules—give your code clarity, flexibility, and structure. This post offers a gentle, practical introduction to each, with tiny examples you can paste into a .ts file and run.
You don't need any framework knowledge—just TypeScript and either Node.js (with ts-node/tsx) or Bun.
1) Enums — Naming a Set of Related Values
Enums let you define a small set of allowed values with readable names.
Basic enum
enum Role { Admin, Editor, Viewer }
const userRole: Role = Role.Editor;
if (userRole === Role.Admin) {
console.log("Full access");
} else {
console.log("Limited access");
}
- By default, the first member is
0, then1,2, … (Admin = 0,Editor = 1, etc.). - Use when you have a fixed set of related options.
String Enums (recommended for APIs & logs)
enum OrderStatus {
Pending = "PENDING",
Shipped = "SHIPPED",
Delivered = "DELIVERED",
Cancelled = "CANCELLED",
}
String enums are stable (values don't shift if you reorder members) and readable in logs and network messages.
Alternatives to enum: Union of string literals
type OrderStatus2 = "PENDING" | "SHIPPED" | "DELIVERED" | "CANCELLED";
function setStatus(s: OrderStatus2) { /* ... */ }
- Lighter-weight and tree-shakeable.
- Great when you just need a type, not a runtime object.
Rule of thumb: Prefer string literal unions for pure typing; use enum when you also need the runtime object (e.g., Object.values(OrderStatus) for iteration).
2) Generics — Reusable Types That Adapt
Generics let functions, classes, and types work with many shapes while keeping strong type safety.
Generic Function
function wrap<T>(value: T): { data: T } {
return { data: value };
}
const a = wrap(42); // { data: number }
const b = wrap("hello"); // { data: string }
TypeScript infers T automatically from the argument.
Constraining Generics
interface HasId { id: string }
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(i => i.id === id);
}
const users = [{ id: "u1", name: "Ada" }, { id: "u2", name: "Linus" }];
const found = findById(users, "u2"); // found: { id: string; name: string } | undefined
T extends HasId ensures only items with an id are allowed.
Generic Types & Interfaces
type ApiResponse<T> = { ok: true; data: T } | { ok: false; error: string };
function ok<T>(data: T): ApiResponse<T> {
return { ok: true, data };
}
Generic Classes
class Box<T> {
constructor(public value: T) {}
map<U>(fn: (x: T) => U): Box<U> {
return new Box(fn(this.value));
}
}
const numBox = new Box(5).map(n => n * 2); // Box<number>
When generics shine: reusable helpers (data fetching, caching), collections, UI components, or SDK wrappers.
3) Modules — Organizing Code Across Files
Modules let you split code into files and import/export what you need.
Exporting & Importing
math.ts
export function sum(a: number, b: number) {
return a + b;
}
export const PI = 3.14159;
app.ts
import { sum, PI } from "./math";
console.log(sum(2, 3), PI);
Default vs Named Exports
// user.ts
export default function createUser(name: string) {
return { id: Date.now().toString(), name };
}
// main.ts
import createUser from "./user";
- Named exports (preferred): clearer auto-imports & refactors.
- Default export: good for a file’s primary value (one thing per file).
Re-exporting (Barrel Files)
// api/index.ts
export * from "./client";
export * from "./types";
Consumers can now import from "./api" instead of deep paths.
Module Resolution Tips
- In
tsconfig.json, set"module": "ESNext"(or"NodeNext"for Node 18+), and consider paths/aliases:{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }
Putting It Together: Tiny Demo
Goal: model a list of tasks using an enum for status, generics for API result, and modules for structure.
types.ts
export enum TaskStatus { Todo = "TODO", Doing = "DOING", Done = "DONE" }
export interface Task {
id: string;
title: string;
status: TaskStatus;
}
result.ts
export type Result<T> = { ok: true; data: T } | { ok: false; error: string };
export const ok = <T>(data: T): Result<T> => ({ ok: true, data });
export const fail = (error: string): Result<never> => ({ ok: false, error });
repo.ts
import { Task, TaskStatus } from "./types";
import { Result, ok, fail } from "./result";
const db: Task[] = [
{ id: "1", title: "Write blog post", status: TaskStatus.Todo },
{ id: "2", title: "Review PR", status: TaskStatus.Doing },
];
export function getTasks(): Result<Task[]> {
return ok(db);
}
export function addTask(title: string): Result<Task> {
if (!title.trim()) return fail("Title required");
const task: Task = { id: Date.now().toString(), title, status: TaskStatus.Todo };
db.push(task);
return ok(task);
}
main.ts
import { addTask, getTasks } from "./repo";
console.log(getTasks());
console.log(addTask("Ship feature"));
Run with Bun:
bun run main.ts
Or with Node (using tsx):
node --import tsx main.ts
Common Pitfalls (and Gentle Fixes)
- Enums vs unions: prefer string literal unions if you only need types; use
enumif you need runtime values too. - Any everywhere: avoid
any; try generics with constraints (<T extends Foo>). - Circular module imports: use barrel files carefully; split shared types to a separate file (
types.ts). - Config mismatch: align
"module","moduleResolution", and your runtime (Node, Bun, tooling).
Cheat Sheet
- Enum
enum Color { Red = "RED", Green = "GREEN" } - Generic
function id<T>(x: T): T { return x; } - Module
export const x = 1; import { x } from "./file";
Next Steps & References
- TypeScript Handbook — Enums: https://www.typescriptlang.org/docs/handbook/enums.html
- Generics: https://www.typescriptlang.org/docs/handbook/2/generics.html
- Modules: https://www.typescriptlang.org/docs/handbook/2/modules.html
- TSConfig Reference: https://www.typescriptlang.org/tsconfig
Keep it gentle: start with simple shapes, add types gradually, and reach for generics when duplication appears. With these three building blocks, your TypeScript code will be clear, safe, and scalable.
Exploring Modules in JavaScript and TypeScript: import & export Explained
Learn how JavaScript and TypeScript use modules to organize code with import/export — covering CommonJS vs ESM, default vs named exports, re-exports, and real project examples.
Next.js vs Nuxt: The Battle of the Meta Frameworks
A deep comparison between Next.js and Nuxt—the two meta frameworks powering React and Vue ecosystems. We explore their philosophies, performance, ecosystems, and what makes each one a cornerstone of modern web development.