Toolmingo
Guides24 min read

TypeScript Developer Roadmap 2025 (Step-by-Step Guide)

The complete TypeScript developer roadmap for 2025 — JS prerequisites, type system mastery, generics, frameworks, testing, and landing your first TypeScript job. Know exactly what to learn and in what order.

TypeScript is JavaScript with types — and in 2025 it's the de facto standard for professional JavaScript development. It's used by every major tech company, powers Angular by design, and is the preferred choice for React, Vue, Node.js, and Deno. Stack Overflow's 2024 survey shows TypeScript as the 5th most-used language overall and the most-loved typed language. This roadmap shows you exactly what to learn, in what order, and realistic timelines to go from zero to a job-ready TypeScript developer.

At a glance

Phase Topics Time estimate
0 JavaScript prerequisites 4–8 weeks
1 TypeScript fundamentals — types, interfaces, functions 3–4 weeks
2 Type system depth — generics, utility types, narrowing 4–5 weeks
3 Object-oriented TypeScript — classes, decorators, patterns 2–3 weeks
4 Configuration and tooling — tsconfig, bundlers, linters 1–2 weeks
5 Frontend with TypeScript — React or Vue (your pick) 4–6 weeks
6 Backend with TypeScript — Node.js, Express, NestJS 4–5 weeks
7 Databases and ORMs — Prisma, Drizzle, TypeORM 2–3 weeks
8 Testing — Jest, Vitest, type-safe mocks 2–3 weeks
9 Advanced patterns — conditional types, branded types, DI 3–4 weeks
10 Portfolio projects and job search 4–8 weeks
Total to first job ~10–14 months

Phase 0 — JavaScript prerequisites (Weeks 1–8)

TypeScript is a superset of JavaScript. Every JavaScript program is a valid TypeScript program — so you must know JS first.

Core JavaScript you need

// 1. Variables and scoping (let/const — no var)
const name = "Alice";
let count = 0;

// 2. Arrow functions
const add = (a, b) => a + b;

// 3. Destructuring and spread
const { x, y, ...rest } = point;
const merged = { ...defaults, ...overrides };

// 4. Array methods (must know all of these)
const doubled = nums.map(n => n * 2);
const evens   = nums.filter(n => n % 2 === 0);
const sum      = nums.reduce((acc, n) => acc + n, 0);

// 5. Promises and async/await
const data = await fetch(url).then(r => r.json());

// 6. Modules (ESM)
export const greet = (name) => `Hello, ${name}`;
import { greet } from "./greet.js";

// 7. Classes
class Animal {
  constructor(name) { this.name = name; }
  speak() { return `${this.name} makes a noise.`; }
}

// 8. Optional chaining and nullish coalescing
const city = user?.address?.city ?? "Unknown";

JavaScript concepts to understand before TypeScript

Concept Why it matters for TypeScript
Prototypal inheritance TS classes compile to this
Closures and scope Required to understand generic inference
this binding rules Critical for class methods and decorators
Event loop basics Async TypeScript builds on this
Array/object mutation vs immutability TypeScript readonly builds on this
ESM vs CommonJS modules tsconfig module setting depends on this
Browser vs Node.js environment Determines which lib types to include

Recommended resources: javascript.info (free, excellent), "You Don't Know JS" (free on GitHub), freeCodeCamp JavaScript Algorithms.

You're ready for TypeScript when you can build a small REST API in vanilla Node.js without looking up syntax.


Phase 1 — TypeScript fundamentals (Weeks 9–12)

Installing and running TypeScript

npm install -g typescript        # install globally
tsc --version                    # verify

# Or per-project (preferred)
npm init -y
npm install -D typescript @types/node
npx tsc --init                   # generate tsconfig.json

# Run TypeScript files
npx ts-node src/index.ts         # dev mode
npx tsx src/index.ts             # faster alternative (tsx package)
tsc && node dist/index.js        # compile then run

Primitive types and type annotations

// Explicit annotations
let age: number = 30;
let name: string = "Alice";
let active: boolean = true;
let nothing: null = null;
let missing: undefined = undefined;

// TypeScript infers most types — annotations are optional
let count = 0;                  // inferred: number
const greeting = "hello";       // inferred: "hello" (literal type!)

// Arrays
let scores: number[] = [95, 87, 92];
let tags: Array<string> = ["ts", "js"];

// Tuples — fixed length, fixed types
let point: [number, number] = [10, 20];
let entry: [string, number] = ["Alice", 30];

// Enums
enum Direction { Up, Down, Left, Right }
const move = Direction.Up;

// Const enums (no runtime object, faster)
const enum Status { Active = "ACTIVE", Inactive = "INACTIVE" }

any, unknown, and never

// any — opts out of type checking (avoid in production code)
let x: any = "hello";
x = 42;          // no error
x.foo.bar;       // no error — dangerous!

// unknown — type-safe alternative to any
let input: unknown = getUserInput();
if (typeof input === "string") {
  console.log(input.toUpperCase()); // OK after type narrowing
}

// never — a type that never occurs (exhaustive checks, infinite loops)
function throwError(msg: string): never {
  throw new Error(msg);
}

function assertNever(value: never): never {
  throw new Error(`Unhandled value: ${JSON.stringify(value)}`);
}

Interfaces and type aliases

// Interface — describes object shape
interface User {
  id: number;
  name: string;
  email?: string;           // optional property
  readonly createdAt: Date; // read-only property
}

// Type alias — more flexible
type Point = { x: number; y: number };
type ID = string | number;  // union type
type Status = "active" | "inactive" | "pending"; // string literal union

// Key difference: interfaces can be extended/reopened
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }

// Types can use intersection
type Dog = Animal & { breed: string };

// When to use which?
// - interface: for object shapes, especially public APIs that others extend
// - type alias: for unions, intersections, primitives, tuples, computed types

Functions

// Named function with annotations
function add(a: number, b: number): number {
  return a + b;
}

// Arrow function
const multiply = (a: number, b: number): number => a * b;

// Optional and default parameters
function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

// Rest parameters
function sum(...numbers: number[]): number {
  return numbers.reduce((a, b) => a + b, 0);
}

// Function types
type MathFn = (a: number, b: number) => number;
const divide: MathFn = (a, b) => a / b;

// Overloads — multiple signatures for one function
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
  return String(value);
}

Phase 2 — Type system depth (Weeks 13–17)

This is where TypeScript gets powerful — and where junior devs usually stop learning.

Union and intersection types

// Union — either/or
type StringOrNumber = string | number;
type Theme = "light" | "dark";

// Discriminated union — the most important pattern
type Shape =
  | { kind: "circle";    radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle";  base: number;  height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":    return Math.PI * shape.radius ** 2;
    case "rectangle": return shape.width * shape.height;
    case "triangle":  return 0.5 * shape.base * shape.height;
    // TypeScript knows all cases are covered — no default needed
  }
}

// Intersection — combine types
type Timestamped = { createdAt: Date; updatedAt: Date };
type User = { id: number; name: string };
type UserRecord = User & Timestamped; // has all four properties

Type narrowing

// typeof narrowing
function process(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase(); // TypeScript knows it's string here
  }
  return value.toFixed(2); // TypeScript knows it's number here
}

// instanceof narrowing
function handleError(error: unknown) {
  if (error instanceof Error) {
    console.log(error.message); // OK
  }
}

// in narrowing
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function makeSound(animal: Cat | Dog) {
  if ("meow" in animal) {
    animal.meow();
  } else {
    animal.bark();
  }
}

// Custom type guard
function isString(value: unknown): value is string {
  return typeof value === "string";
}

// Assertion functions
function assertDefined<T>(value: T | null | undefined): asserts value is T {
  if (value == null) throw new Error("Value is not defined");
}

Generics

// Generic function
function identity<T>(value: T): T {
  return value;
}
identity<string>("hello"); // explicit
identity(42);              // inferred: identity<number>

// Generic interface
interface Repository<T> {
  findById(id: number): Promise<T | null>;
  findAll(): Promise<T[]>;
  save(entity: T): Promise<T>;
  delete(id: number): Promise<void>;
}

// Generic class
class Stack<T> {
  private items: T[] = [];
  push(item: T): void { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
  peek(): T | undefined { return this.items[this.items.length - 1]; }
}

// Generic constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// Multiple type parameters
function zip<T, U>(arr1: T[], arr2: U[]): [T, U][] {
  return arr1.map((item, i) => [item, arr2[i]]);
}

// Default type parameters
interface ApiResponse<T = unknown> {
  data: T;
  status: number;
  message: string;
}

Utility types

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  role: "admin" | "user";
}

// Partial<T> — all properties optional
type PartialUser = Partial<User>;
// { id?: number; name?: string; ... }

// Required<T> — all properties required
type RequiredUser = Required<PartialUser>;

// Pick<T, K> — select specific properties
type PublicUser = Pick<User, "id" | "name" | "email">;

// Omit<T, K> — exclude specific properties
type UserWithoutPassword = Omit<User, "password">;

// Record<K, V> — dictionary type
type RolePermissions = Record<User["role"], string[]>;

// Readonly<T> — immutable version
type ImmutableUser = Readonly<User>;

// NonNullable<T> — remove null and undefined
type SafeString = NonNullable<string | null | undefined>; // string

// ReturnType<T> — extract function return type
function createUser(): User { /* ... */ return {} as User; }
type CreatedUser = ReturnType<typeof createUser>; // User

// Parameters<T> — extract function parameters as tuple
type CreateArgs = Parameters<typeof createUser>; // []

// Awaited<T> — unwrap Promise
type ResolvedUser = Awaited<Promise<User>>; // User

Conditional and mapped types

// Conditional type
type IsArray<T> = T extends any[] ? true : false;
type R1 = IsArray<string[]>; // true
type R2 = IsArray<string>;   // false

// infer keyword
type UnpackArray<T> = T extends (infer U)[] ? U : T;
type R3 = UnpackArray<string[]>; // string
type R4 = UnpackArray<string>;   // string

// Mapped type — transform all properties
type Optional<T> = { [K in keyof T]?: T[K] };
type Mutable<T>  = { -readonly [K in keyof T]: T[K] };
type Nullable<T> = { [K in keyof T]: T[K] | null };

// Template literal types
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickHandler = EventName<"click">; // "onClick"

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; ... }

Phase 3 — Object-oriented TypeScript (Weeks 18–20)

Classes

class BankAccount {
  // Access modifiers
  public readonly id: string;
  private balance: number;
  protected owner: string;

  constructor(owner: string, initialBalance = 0) {
    this.id = crypto.randomUUID();
    this.owner = owner;
    this.balance = initialBalance;
  }

  // Getter and setter
  get currentBalance(): number { return this.balance; }
  set currentBalance(amount: number) {
    if (amount < 0) throw new Error("Balance cannot be negative");
    this.balance = amount;
  }

  deposit(amount: number): void {
    if (amount <= 0) throw new Error("Amount must be positive");
    this.balance += amount;
  }

  // Static method
  static fromJSON(data: { owner: string; balance: number }): BankAccount {
    return new BankAccount(data.owner, data.balance);
  }
}

// Shorthand: constructor parameter properties
class Point {
  constructor(
    public readonly x: number,
    public readonly y: number,
  ) {}
  distance(): number { return Math.sqrt(this.x ** 2 + this.y ** 2); }
}

Abstract classes and interfaces

// Abstract class — cannot be instantiated, provides base implementation
abstract class Logger {
  abstract log(message: string): void;

  info(message: string): void { this.log(`[INFO] ${message}`); }
  error(message: string): void { this.log(`[ERROR] ${message}`); }
}

class ConsoleLogger extends Logger {
  log(message: string): void { console.log(message); }
}

// Interface implementation
interface Serializable {
  serialize(): string;
  deserialize(data: string): this;
}

class Config implements Serializable {
  constructor(private data: Record<string, unknown> = {}) {}
  serialize(): string { return JSON.stringify(this.data); }
  deserialize(data: string): this {
    return Object.assign(Object.create(Object.getPrototypeOf(this)), {
      data: JSON.parse(data),
    });
  }
}

Decorators (TypeScript 5 + Stage 3)

// TypeScript 5 decorators (Stage 3 proposal — stable)
// tsconfig: "experimentalDecorators": false (use new decorators)

function log(target: any, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name);
  return function (this: any, ...args: any[]) {
    console.log(`Calling ${methodName} with`, args);
    const result = target.call(this, ...args);
    console.log(`${methodName} returned`, result);
    return result;
  };
}

class Calculator {
  @log
  add(a: number, b: number): number { return a + b; }
}

// Class decorator
function singleton<T extends { new(...args: any[]): {} }>(Base: T) {
  let instance: InstanceType<T>;
  return class extends Base {
    constructor(...args: any[]) {
      if (instance) return instance;
      super(...args);
      instance = this as InstanceType<T>;
    }
  };
}

Phase 4 — Configuration and tooling (Weeks 21–22)

tsconfig.json essentials

{
  "compilerOptions": {
    // Target and module format
    "target": "ES2022",         // compiled JS version
    "module": "NodeNext",        // ESM for Node.js; "ESNext" for bundlers
    "moduleResolution": "NodeNext",

    // Type safety (enable all)
    "strict": true,              // enables all strict checks
    "noUncheckedIndexedAccess": true, // arr[0] is T | undefined
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,

    // Output
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,         // emit .d.ts files (for libraries)
    "declarationMap": true,      // source maps for declarations
    "sourceMap": true,

    // Path aliases
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },

    // Interop
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

Build tools and bundlers

Tool Use case Config file
tsc Simple compilation, libraries tsconfig.json
ts-node / tsx Development scripts tsconfig.json
Vite Frontend apps (React/Vue) vite.config.ts
esbuild Fast bundling, CLI tools build.js or API
Rollup Libraries with tree-shaking rollup.config.ts
Webpack Enterprise apps, custom loaders webpack.config.ts
Turbopack Next.js (built-in) next.config.ts
Bun Runtime + bundler + test runner bunfig.toml

ESLint and Prettier

# TypeScript ESLint (replaces TSLint, which is deprecated)
npm install -D eslint @eslint/js typescript-eslint prettier

# eslint.config.mjs (flat config — ESLint 9+)
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.strictTypeChecked,
  {
    languageOptions: {
      parserOptions: { project: true, tsconfigRootDir: import.meta.dirname },
    },
    rules: {
      "@typescript-eslint/no-explicit-any": "error",
      "@typescript-eslint/consistent-type-imports": "warn",
    },
  },
);
// .prettierrc
{
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100
}

Phase 5 — Frontend with TypeScript (Weeks 23–28)

React + TypeScript

// Component props
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary" | "danger";
  disabled?: boolean;
  children?: React.ReactNode;
}

export function Button({ label, onClick, variant = "primary", disabled = false }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

// Typed hooks
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);

const inputRef = useRef<HTMLInputElement>(null);

// Generic component
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Typed event handlers
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  setQuery(event.target.value);
}

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
  event.preventDefault();
}

// Custom hook with types
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
  const [stored, setStored] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value: T) => {
    setStored(value);
    window.localStorage.setItem(key, JSON.stringify(value));
  };

  return [stored, setValue];
}

Next.js 15 App Router + TypeScript

// app/users/[id]/page.tsx
interface PageProps {
  params: Promise<{ id: string }>;
  searchParams: Promise<{ tab?: string }>;
}

export default async function UserPage({ params, searchParams }: PageProps) {
  const { id } = await params;
  const { tab = "profile" } = await searchParams;
  const user = await getUser(id);
  return <UserProfile user={user} tab={tab} />;
}

// Type-safe Server Actions
"use server";
import { z } from "zod";

const CreateUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
});

export async function createUser(formData: FormData) {
  const result = CreateUserSchema.safeParse({
    name: formData.get("name"),
    email: formData.get("email"),
  });
  if (!result.success) return { error: result.error.flatten() };
  // save to database...
}

Frontend TypeScript libraries to know

Category Library TypeScript quality
State management Zustand, Jotai Excellent (TS-first)
Data fetching TanStack Query Excellent (TS-first)
Forms React Hook Form Excellent (TS-first)
Schema validation Zod, Valibot Excellent (type inference)
UI components shadcn/ui, Radix UI Excellent
Routing (React) React Router v7, TanStack Router Excellent
Animation Framer Motion Good
Charts Recharts, Victory Good
Dates date-fns, Temporal API Excellent

Phase 6 — Backend with TypeScript (Weeks 29–33)

Node.js + Express

// Type-safe Express
import express, { Request, Response, NextFunction } from "express";
import { z } from "zod";

const app = express();
app.use(express.json());

// Typed request body with Zod
const CreateUserBody = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
});
type CreateUserBody = z.infer<typeof CreateUserBody>;

function validateBody<T>(schema: z.ZodType<T>) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({ errors: result.error.flatten() });
    }
    req.body = result.data;
    next();
  };
}

app.post("/users", validateBody(CreateUserBody), async (req: Request, res: Response) => {
  const { name, email } = req.body as CreateUserBody;
  const user = await userService.create({ name, email });
  res.status(201).json(user);
});

// Global error handler (4-argument)
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err.stack);
  res.status(500).json({ error: err.message });
});

NestJS (TypeScript-first Node.js framework)

// NestJS is built with TypeScript by design
import { Controller, Get, Post, Body, Param, NotFoundException } from "@nestjs/common";
import { IsEmail, IsString, MinLength } from "class-validator";

export class CreateUserDto {
  @IsString() @MinLength(2) name: string;
  @IsEmail() email: string;
}

@Controller("users")
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() { return this.usersService.findAll(); }

  @Get(":id")
  async findOne(@Param("id") id: string) {
    const user = await this.usersService.findOne(+id);
    if (!user) throw new NotFoundException(`User #${id} not found`);
    return user;
  }

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
}

Backend TypeScript ecosystem

Category Options Notes
Framework Express, Fastify, Hono, NestJS, Elysia NestJS for enterprise; Hono/Elysia for edge
Runtime Node.js, Bun, Deno Bun/Deno have native TS support
Validation Zod, Valibot, class-validator Zod is ecosystem standard
Auth Lucia, Auth.js, Better Auth Lucia for custom; Auth.js for Next.js
Job queues BullMQ, Trigger.dev Both have excellent TS types
HTTP client fetch (built-in), Axios, ky ky is typed fetch wrapper
Logging Pino, Winston Pino is faster
Config dotenv, env-var, t3-env t3-env for type-safe env vars

Phase 7 — Databases and ORMs (Weeks 34–36)

Prisma (TypeScript ORM — most popular)

// schema.prisma
model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id       Int    @id @default(autoincrement())
  title    String
  content  String?
  author   User   @relation(fields: [authorId], references: [id])
  authorId Int
}
// Prisma generates full TypeScript types from schema
import { PrismaClient, Prisma } from "@prisma/client";

const prisma = new PrismaClient();

// Create — fully typed
const user = await prisma.user.create({
  data: { name: "Alice", email: "alice@example.com" },
  include: { posts: true },
});
// user is typed as User & { posts: Post[] }

// Find with relation — typed result
const users = await prisma.user.findMany({
  where: { email: { endsWith: "@example.com" } },
  include: { posts: { where: { content: { not: null } } } },
  orderBy: { createdAt: "desc" },
  take: 10,
});

// Transaction
const [post, updatedUser] = await prisma.$transaction([
  prisma.post.create({ data: { title: "Hello", authorId: user.id } }),
  prisma.user.update({ where: { id: user.id }, data: { name: "Alice Updated" } }),
]);

// Type helpers
type UserWithPosts = Prisma.UserGetPayload<{ include: { posts: true } }>;

Drizzle ORM (SQL-first, ultra type-safe)

// schema.ts
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id:        serial("id").primaryKey(),
  name:      text("name").notNull(),
  email:     text("email").notNull().unique(),
  createdAt: timestamp("created_at").defaultNow(),
});

// queries.ts
import { db } from "./db";
import { users } from "./schema";
import { eq, like } from "drizzle-orm";

// Select — inferred return type
const allUsers = await db.select().from(users);
// typeof allUsers = { id: number; name: string; email: string; createdAt: Date | null }[]

// Where clause
const alice = await db.select().from(users).where(eq(users.email, "alice@example.com"));

// Insert with returning
const [newUser] = await db.insert(users).values({ name: "Bob", email: "bob@example.com" }).returning();

Phase 8 — Testing with TypeScript (Weeks 37–38)

Jest + ts-jest (or Vitest)

// jest.config.ts
import type { Config } from "jest";

const config: Config = {
  preset: "ts-jest",
  testEnvironment: "node",
  moduleNameMapper: { "^@/(.*)$": "<rootDir>/src/$1" },
};
export default config;
// user.service.test.ts
import { UserService } from "./user.service";
import { type UserRepository } from "./user.repository";

// Type-safe mocking
const mockRepo: jest.Mocked<UserRepository> = {
  findById: jest.fn(),
  findAll: jest.fn(),
  save: jest.fn(),
  delete: jest.fn(),
};

describe("UserService", () => {
  let service: UserService;

  beforeEach(() => {
    jest.clearAllMocks();
    service = new UserService(mockRepo);
  });

  it("should return user when found", async () => {
    const mockUser: User = { id: 1, name: "Alice", email: "alice@example.com" };
    mockRepo.findById.mockResolvedValue(mockUser);

    const result = await service.getUser(1);

    expect(result).toEqual(mockUser);
    expect(mockRepo.findById).toHaveBeenCalledWith(1);
  });

  it("should throw when user not found", async () => {
    mockRepo.findById.mockResolvedValue(null);
    await expect(service.getUser(999)).rejects.toThrow("User not found");
  });
});

Vitest (faster alternative, identical API)

// vitest.config.ts
import { defineConfig } from "vitest/config";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [tsconfigPaths()],
  test: {
    environment: "node",
    coverage: { reporter: ["text", "html"], thresholds: { lines: 80 } },
  },
});
// user.service.test.ts (Vitest — same syntax as Jest)
import { describe, it, expect, vi, beforeEach } from "vitest";

const mockRepo = {
  findById: vi.fn<[number], Promise<User | null>>(),
  // ...
};

Testing types (tsd, expect-type)

// Type-level tests with expect-type
import { expectType, expectError } from "tsd";

// Test that utility types work correctly
expectType<string>(identity("hello"));
expectType<number>(identity(42));
expectError(identity<string>(42)); // should fail type check

Phase 9 — Advanced TypeScript patterns (Weeks 39–42)

Branded types (newtype pattern)

// Prevent mixing semantically different values of the same type
type UserId  = string & { readonly brand: "UserId" };
type OrderId = string & { readonly brand: "OrderId" };

function createUserId(id: string): UserId   { return id as UserId; }
function createOrderId(id: string): OrderId { return id as OrderId; }

function getUser(id: UserId): User { /* ... */ return {} as User; }

const userId  = createUserId("user-123");
const orderId = createOrderId("order-456");

getUser(userId);    // OK
getUser(orderId);   // Type error! Cannot use OrderId where UserId expected

Builder pattern with method chaining

class QueryBuilder<T> {
  private conditions: string[] = [];
  private selectedFields: (keyof T)[] = [];
  private limitValue?: number;

  select(...fields: (keyof T)[]): this {
    this.selectedFields = fields;
    return this;
  }

  where(condition: string): this {
    this.conditions.push(condition);
    return this;
  }

  limit(n: number): this {
    this.limitValue = n;
    return this;
  }

  build(): string {
    const fields = this.selectedFields.length
      ? this.selectedFields.join(", ")
      : "*";
    const where = this.conditions.length
      ? `WHERE ${this.conditions.join(" AND ")}`
      : "";
    const limit = this.limitValue ? `LIMIT ${this.limitValue}` : "";
    return `SELECT ${fields} ${where} ${limit}`.trim();
  }
}

// Usage
const query = new QueryBuilder<User>()
  .select("id", "name")
  .where("active = true")
  .limit(10)
  .build();

Dependency injection with TypeScript

// Interface-based DI — testable, swappable implementations
interface Logger {
  info(msg: string): void;
  error(msg: string): void;
}

interface UserRepository {
  findAll(): Promise<User[]>;
  findById(id: number): Promise<User | null>;
  save(user: Omit<User, "id">): Promise<User>;
}

// Service depends on interfaces, not implementations
class UserService {
  constructor(
    private readonly repo: UserRepository,
    private readonly logger: Logger,
  ) {}

  async getAll(): Promise<User[]> {
    this.logger.info("Fetching all users");
    return this.repo.findAll();
  }
}

// Production wiring
const service = new UserService(
  new PostgresUserRepository(db),
  new PinoLogger(),
);

// Test wiring
const service = new UserService(
  new InMemoryUserRepository(),
  new NoopLogger(),
);

Type-safe event emitter

// Strongly typed events — no more string-based emit("event")
type EventMap = {
  "user:created": { user: User; timestamp: Date };
  "user:deleted": { userId: number };
  "order:placed":  { order: Order; total: number };
};

class TypedEventEmitter<Events extends Record<string, unknown>> {
  private listeners = new Map<keyof Events, Set<(data: any) => void>>();

  on<K extends keyof Events>(event: K, listener: (data: Events[K]) => void): void {
    if (!this.listeners.has(event)) this.listeners.set(event, new Set());
    this.listeners.get(event)!.add(listener);
  }

  emit<K extends keyof Events>(event: K, data: Events[K]): void {
    this.listeners.get(event)?.forEach(listener => listener(data));
  }
}

const emitter = new TypedEventEmitter<EventMap>();
emitter.on("user:created", ({ user, timestamp }) => {
  console.log(`User ${user.name} created at ${timestamp}`); // fully typed!
});

Technology map

TypeScript Ecosystem
├── Language
│   ├── Type system (primitives, generics, utility types, conditional types)
│   ├── tsc compiler (5.x — decorators, const type params, isolated modules)
│   └── tsconfig.json (strict mode, paths, module formats)
│
├── Runtimes
│   ├── Node.js (CommonJS + ESM via tsconfig)
│   ├── Bun (native TS, no transpilation needed)
│   └── Deno (native TS, permissions model)
│
├── Frontend
│   ├── React + Next.js (most popular)
│   ├── Vue 3 + Nuxt (Volar TS support)
│   ├── Angular (TS-first by design)
│   └── Svelte 5 (runes — TS-first)
│
├── Backend
│   ├── Express / Fastify / Hono
│   ├── NestJS (decorators + DI — enterprise TS)
│   └── Elysia (Bun-first, ultra-typed)
│
├── Databases
│   ├── Prisma (schema-first, generates types)
│   ├── Drizzle (SQL-first, infers types)
│   └── TypeORM (entity decorators)
│
├── Validation
│   ├── Zod (most popular, runtime + types)
│   └── Valibot (smaller, tree-shakeable)
│
└── Testing
    ├── Jest + ts-jest
    ├── Vitest (fast, Vite-powered)
    └── Playwright (E2E, typed page objects)

12-month learning timeline

Month Focus Goal
1–2 JavaScript fundamentals Build 3 vanilla JS projects
3 TypeScript basics Convert a JS project to TS
4 Type system depth (generics, utility types) Write zero any in new code
5 React + TypeScript Build typed todo app with hooks
6 Node.js + TypeScript APIs Build CRUD REST API
7 Database + ORM (Prisma) Add persistence to API
8 Auth + Security (JWT/session) Add user accounts
9 Testing (Jest/Vitest) Reach 80% coverage
10 Advanced patterns (branded types, DI) Refactor project to use patterns
11 Full-stack project Deploy complete app
12 Job prep + second project 2 polished GitHub projects

Portfolio project ideas

Project Stack Skills demonstrated
Personal finance tracker Next.js + Prisma + Zod Full-stack CRUD, form validation, charts
Real-time chat app Node.js + Socket.IO + React WebSockets, real-time state, generics
REST API with auth Express/Hono + JWT + Prisma Backend TS, middleware, security
CLI dev tool Node.js + Commander.js CLI development, file I/O
Open-source contribution Any popular TS repo Reading existing TS, PR workflow
Type-safe component library React + Rollup + Storybook Library architecture, declarations

TypeScript developer roles and salaries

Role TypeScript use US median Remote
Frontend developer Daily (React/Vue) $110–140k Common
Full-stack developer Daily (Next.js + Node) $120–155k Very common
Backend developer Daily (Node.js/Deno) $115–145k Common
React/Next.js specialist Daily $130–160k Very common
Angular developer Daily (TS-first) $115–145k Common
NestJS backend engineer Daily $120–150k Common
TypeScript library author Daily $140–180k Very common

TypeScript knowledge adds a +15–25% salary premium over vanilla JavaScript developers for the same role, according to Stack Overflow 2024 and Glassdoor data.


Common mistakes

Mistake Better approach
Using any everywhere Use unknown with type guards; enable noImplicitAny
Type assertion as X instead of narrowing Narrow with typeof, instanceof, or custom guards
Duplicate type definitions (interface + class) Use typeof ClassName or derive types from Zod schemas
@ts-ignore instead of fixing the error Fix the actual type error; use @ts-expect-error with comment
Over-engineering with complex conditional types Start simple; add complexity only when needed
Forgetting strict: true in tsconfig Enable it from day one — much harder to add later
Not using readonly for immutable data Mark all inputs as readonly to prevent accidental mutation
Importing types as values Use import type { User } for type-only imports

TypeScript vs JavaScript vs Flow vs JSDoc

Feature TypeScript JavaScript Flow JSDoc types
Static type checking Full None Full Partial
IDE autocomplete Excellent Limited Good Good
Compile step needed Yes (or ts-node) No Yes No
Learning curve Medium Low Medium Low
Ecosystem support Excellent (standard) Universal Declining Limited
Generics Yes N/A Yes Partial
Utility types Yes N/A Partial No
Strictness control Granular N/A Granular Minimal
Used by Angular Required No No No
npm packages @types/* N/A Flow types N/A

Frequently asked questions

Q: Do I need to learn JavaScript before TypeScript?
Yes — TypeScript knowledge doesn't replace JavaScript knowledge. You'll still debug runtime JS errors, read untyped third-party code, and use the browser's JS API. Learn JavaScript for 4–8 weeks first. Then TypeScript will make immediate sense.

Q: Should I use TypeScript for small projects or scripts?
For scripts under ~200 lines you might use JSDoc type comments (/** @type {string} */) instead of a full TypeScript compile step. For anything bigger, TypeScript pays off quickly. With tsx or Bun you can run TS files directly without a build step.

Q: How strict should my tsconfig be?
Enable strict: true from day one. It catches real bugs (null pointer dereferences, implicit any) and makes your code much safer. Adding strictness to an existing loose codebase is painful — start strict.

Q: TypeScript or JSDoc-only types?
JSDoc is great for libraries that want to ship a single JS file with type hints for consumers. For application code, TypeScript's full type system (generics, conditional types, mapped types) is far more capable. Use TypeScript.

Q: What about Deno and Bun — do they change TypeScript development?
Both run TypeScript natively without a separate compile step, which removes tooling friction. Bun is production-ready and popular for its speed. Deno 2 has improved Node.js compatibility. But the TypeScript language itself is identical — the skills transfer directly.

Q: How do I get TypeScript types for a package that doesn't have them?
First check @types/<package> on npm — the DefinitelyTyped project covers thousands of popular packages. If no types exist, you can write a .d.ts declaration file yourself or use declare module "package-name" to tell TypeScript to treat it as any.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools