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.