Blog
Oct 12, 2025 - 11 MIN READ
A Gentle Introduction to Enums, Generics, and Modules in TypeScript

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

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.


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, then 1, 2, … (Admin = 0, Editor = 1, etc.).
  • Use when you have a fixed set of related options.
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)

  1. Enums vs unions: prefer string literal unions if you only need types; use enum if you need runtime values too.
  2. Any everywhere: avoid any; try generics with constraints (<T extends Foo>).
  3. Circular module imports: use barrel files carefully; split shared types to a separate file (types.ts).
  4. 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

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.

Built by DOGUNFX using Nuxt UI • © 2025