TypeScript: The Practical Guide for JavaScript Developers (2026)
TypeScript isn't just "JavaScript with types" — it's a superpower that catches bugs before they reach production. Here's what you actually need to know.
Why TypeScript Matters
// JavaScript: Runtime errors = angry users
function calculateDiscount(price, isPremium) {
return price * (isPremium ? 0.9 : 0.95); // What if price is a string?
}
calculateDiscount("100", true); // Returns NaN at runtime! No error before.
// TypeScript: Compile-time errors = happy developers
function calculateDiscount(price: number, isPremium: boolean): number {
return price * (isPremium ? 0.9 : 0.95);
}
calculateDiscount("100", true); // Error: Argument of type 'string' is not assignable to parameter of type 'number'
Type System Essentials
// Primitives
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"];
// Readonly arrays (prevent mutation)
const config: readonly string[] = ["production", "debug"];
// Objects
interface User {
id: string;
name: string;
email?: string; // Optional property
readonly createdAt: Date; // Cannot be modified after creation
role: 'admin' | 'editor' | 'viewer'; // Union literal type
}
// Functions with full typing
function createUser(name: string, role: User['role'] = 'viewer'): User {
return { id: crypto.randomUUID(), name, role, createdAt: new Date() };
}
// Union types
type Status = 'pending' | 'active' | 'completed' | 'failed';
type ID = string | number;
function processItem(id: ID, status: Status): void {
console.log(`Processing ${id}: ${status}`);
}
// Intersection types (combine multiple types)
interface HasTimestamps {
createdAt: Date;
updatedAt: Date;
}
interface SoftDeleteable {
deletedAt: Date | null;
}
type DeletableEntity = HasTimestamps & SoftDeleteable;
// Type narrowing with typeof and instanceof
function formatValue(value: string | number | Date): string {
if (typeof value === 'string') return value.toUpperCase();
if (typeof value === 'number') return `$${value.toFixed(2)}`;
if (value instanceof Date) return value.toLocaleDateString();
return String(value);
}
Advanced Types You'll Use Every Day
// Generics: Reusable types that work with any type
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
timestamp: Date;
}
async function fetchUser(id: string): Promise<ApiResponse<User>> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
async function fetchUsers(): Promise<ApiResponse<User[]>> {
const res = await fetch('/api/users');
return res.json();
}
// Generic function
function first<T>(items: T[]): T | undefined {
return items[0];
}
first([1, 2, 3]); // number | undefined
first(['a', 'b']); // string | undefined
// Utility types (built-in!)
interface Task {
id: string;
title: string;
description: string;
priority: 'low' | 'medium' | 'high';
completed: boolean;
tags: string[];
metadata: Record<string, unknown>;
}
// Pick: Select specific properties
type TaskSummary = Pick<Task, 'id' | 'title' | 'completed'>;
// Same as: { id: string; title: string; completed: boolean; }
// Omit: Exclude specific properties
type CreateTaskInput = Omit<Task, 'id' | 'createdAt'>;
// Partial: Make all properties optional
type UpdateTaskInput = Partial<Task>; // All fields optional for PATCH endpoint
// Required: Make all properties required
type RequiredTask = Required<Pick<Task, 'title' | 'priority'>>;
// Record: Key-value object type
type TaskMap = Record<string, Task>; // { [key: string]: Task }
type RolePermissions = Record<User['role'], string[]>;
// ReturnType: Extract return type of a function
type UserCreator = ReturnType<typeof createUser>; // => User
// Parameters: Extract parameter types as tuple
type CreateUserParams = Parameters<typeof createUser>; // => [string, User['role']?]
// Mapped types
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type OptionalTask = Optional<Task, 'description' | 'tags'>;
// Conditional types
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Template literal types
type EventName = `on${Capitalize<string>}`;
type ClickEvent = 'onClick';
type SubmitEvent = 'onSubmit';
// Keyof type operator
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
getProperty({ name: 'Alice', age: 30 }, 'name'); // string
getProperty({ name: 'Alice', age: 30 }, 'age'); // number
Practical Patterns
// Pattern 1: Discriminated unions (the most powerful TS pattern!)
type Result<T> =
| { status: 'success'; data: T }
| { status: 'error'; error: Error; code: string };
function handleResult(result: Result<User>) {
switch (result.status) {
case 'success':
console.log(result.data.name); // TS knows .data exists here!
break;
case 'error':
console.error(result.code, result.error.message);
break;
}
// No default needed — TS checks exhaustiveness!
}
// Pattern 2: Branded types (prevent mixing up similar primitives)
type UserId = string & { __brand: 'UserId' };
type SessionId = string & { __brand: 'SessionId' };
function createUserId(id: string): UserId { return id as UserId; }
function createSessionId(id: string): SessionId { return id as SessionId; }
function findUser(id: UserId) { /* ... */ }
function validateSession(id: SessionId) { /* ... */ */
const uid = createUserId('abc123');
findUser(uid); // OK
validateSession(uid); // Error! Type 'UserId' is not assignable to 'SessionId'
// Pattern 3: Type guards (runtime + compile-time checking)
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
function assertIsDefined<T>(value: T | null | undefined, name: string): asserts value is T {
if (value === undefined || value === null) throw new Error(`${name} must be defined`);
}
const user = await db.users.findById(id);
assertIsDefined(user, 'user'); // After this line, TS knows user is not null/undefined!
user.name; // Safe!
// Pattern 4: Const assertions (widest possible literal types)
const CONFIG = {
API_URL: 'https://api.example.com',
MAX_RETRIES: 3,
ENDPOINTS: ['/users', '/posts', '/comments'],
} as const;
// CONFIG.API_URL is typed as 'https://api.example.com' (literal), not string!
// Pattern 5: satisfies operator (validate without widening)
type Point = { x: number; y: number };
const p = { x: 1, y: 2 } satisfies Point; // Validates shape, keeps exact type
// Pattern 6: Error handling with never
function exhaustiveCheck(value: never): never {
throw new Error(`Unhandled case: ${value}`);
}
function getStatusMessage(status: Status): string {
switch (status) {
case 'pending': return 'Waiting...';
case 'active': return 'In progress';
case 'completed': return 'Done!';
case 'failed': return 'Failed';
default: return exhaustiveCheck(status); // Compile error if new Status added!
}
}
Configuring TypeScript
// tsconfig.json — recommended strict settings
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"noUncheckedIndexedAccess": true, // arr[0] could be undefined
"noImplicitReturns": true, // All code paths must return
"noFallthroughCasesInSwitch": true, // Prevent missing breaks
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"declaration": true, // Generate .d.ts files
"declarationMap": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"incremental": true, // Faster rebuilds
"tsBuildInfoFile": ".tsbuildinfo"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
What's your favorite TypeScript feature? What confused you when you started?
Follow @armorbreak for more practical developer guides.
Top comments (0)