TypeScript: The Practical Guide for JavaScript Developers (2026)
TypeScript isn't just "JavaScript with types" — it's a superpower that catches bugs before they happen. Here's the practical guide to going from JS to TS.
Why TypeScript Matters
// JavaScript: The bug that only shows in production
function calculateDiscount(price, isMember) {
return price * (isMember ? 0.9 : 0.8); // What if price is "100"? NaN!
}
// TypeScript: Caught at compile time
function calculateDiscount(price: number, isMember: boolean): number {
return price * (isMember ? 0.9 : 0.8);
}
calculateDiscount("100", true); // Error: Argument of type 'string' not assignable to 'number'
Type Basics
// Primitive types
let name: string = "Alice";
let age: number = 30;
let isActive: boolean = true;
let data: null = null;
let nothing: undefined = undefined;
// Arrays
const numbers: number[] = [1, 2, 3];
const names: Array<string> = ["Alice", "Bob"];
// Read-only array
const config: readonly string[] = ["dev", "staging"];
// Objects
interface User {
id: string;
name: string;
email: string;
role?: "admin" | "user"; // Optional + union type
createdAt: Date;
}
const user: User = {
id: crypto.randomUUID(),
name: "Alice",
email: "alice@example.com",
createdAt: new Date(),
};
// Functions with types
function greet(name: string): string { // Return type annotation
return `Hello, ${name}!`;
}
// Arrow function version:
const double = (n: number): number => n * 2;
// Void for functions that don't return
function log(message: string): void {
console.log(message);
}
// Never for functions that never complete
function fail(message: string): never {
throw new Error(message);
}
Interfaces vs Type Aliases
// Interface — best for object shapes (extensible)
interface Product {
id: string;
name: string;
price: number;
category?: string;
}
// Extending interfaces
interface DigitalProduct extends Product {
downloadUrl: string;
fileSize: number;
}
// Type alias — more flexible (unions, tuples, computed types)
type ID = string | number; // Union type
type Status = "pending" | "active" | "archived";
type Pair<T> = [T, T]; // Generic tuple
// Intersection types (combining multiple types)
type WithTimestamps<T> = T & {
createdAt: Date;
updatedAt: Date;
};
type TimestampedProduct = WithTimestamps<Product>;
// When to use which:
// ✅ Interface: Object shapes, class implementation, needs extending
// ✅ Type alias: Unions, intersections, tuples, mapped types, complex computed types
Generics: Reusable Types
// Basic generic function
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
firstElement([1, 2, 3]); // Returns number | undefined
firstElement(["a", "b"]); // Returns string | undefined
// Generic interface
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
timestamp: number;
}
async function fetchUser(id: string): Promise<ApiResponse<User>> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// Generic class
class Repository<T extends { id: string }> {
private items: Map<string, T> = new Map();
async save(item: T): Promise<void> {
this.items.set(item.id, item);
await db.collection('items').doc(item.id).set(item);
}
async find(id: string): Promise<T | null> {
return this.items.get(id) ?? null;
}
async findAll(): Promise<T[]> {
return Array.from(this.items.values());
}
}
// Constrained generics
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
getProperty({ name: "Alice", age: 30 }, "name"); // Returns string
getProperty({ name: "Alice", age: 30 }, "age"); // Returns number
getProperty({ name: "Alice", age: 30 }, "email"); // Error!
Utility Types (Built-in Type Transformers)
// Partial<T> — make all properties optional
function updateProduct(id: string, fields: Partial<Product>): Product {
const existing = db.get(id);
return { ...existing, ...fields };
}
updateProduct("123", { price: 29.99 }); // Only update price
// Required<T> — make all properties required
// Omit<T, K> — remove specific properties
// Pick<T, K> — keep only specific properties
type ProductPreview = Pick<Product, "name" | "price">;
// Record<K, V> — dictionary/object map type
const rolePermissions: Record<User["role"], string[]> = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
};
// Exclude<T, U> — remove from union type
type NonStringPrimitives = Exclude<string | number | boolean, string>; // number | boolean
// ReturnType<F> — get return type of function
type HandlerReturn = ReturnType<typeof handler>;
// Awaited<T> — unwrap Promise type
type UserData = Awaited<Promise<ApiResponse<User>>>; // ApiResponse<User>
// Custom utility type
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Makes ALL nested properties optional recursively
Practical Patterns
// Pattern 1: Discriminated unions (type-safe state machines)
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function renderUI(state: RequestState<User>) {
switch (state.status) {
case "idle": return <p>Click to load</p>;
case "loading": return <Spinner />;
case "success": return <UserProfile data={state.data} />;
case "error": return <ErrorMessage error={state.error} />;
}
// TypeScript knows all cases are handled — no default needed!
}
// Pattern 2: Branded types (prevent mixing similar values)
type UserId = string & { __brand: "UserId" };
type OrderId = string & { __brand: "OrderId" };
function createUserId(id: string): UserId { return id as UserId; }
function createOrderId(id: string): OrderId { return id as OrderId; }
function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ */
const uid = createUserId("abc");
getUser(uid); // OK
getOrder(uid); // Error! UserId is not OrderId — even though both are strings!
// Pattern 3: Const assertions (literal types)
const CONFIG = {
API_URL: "https://api.example.com",
MAX_RETRIES: 3,
TIMEOUT_MS: 5000,
} as const;
// CONFIG.API_URL is typed as literal "https://api.example.com", not string!
// Pattern 4: Template literal types
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiRoute = `/api/${string}`;
type Endpoint = `${HttpMethod} ${ApiRoute}`; // e.g., "GET /api/users"
// Pattern 5: satisfies operator (TypeScript 4.9+)
const colors = {
red: "#ff0000",
green: "#00ff00",
blue: "#0000ff",
} satisfies Record<string, string>; // Validates shape but keeps literal types
Migration Strategy (JS → TS)
# Step 1: Install TypeScript
npm install -D typescript @types/node @types/express
npx tsc --init
# tsconfig.json essentials:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./src",
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
// Migration order:
// 1. Rename .js → .ts (immediate type coverage from inference!)
// 2. Add explicit types to function parameters and returns
// 3. Define interfaces for your data models
// 4. Enable strict mode options one by one
// 5. Add JSDoc @types for third-party libs without .d.ts files
// Quick wins — add types to existing JS files without full migration:
// @ts-check at top of .js file enables type checking!
// /** @type {number} */ for variable annotations
// /** @param {string} name */ for parameter annotations
What's your favorite TypeScript feature? What confused you most when learning it?
Follow @armorbreak for more practical developer guides.
Top comments (0)