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 what you actually need to know.
Why TypeScript Matters
// JavaScript: Runtime errors that should've been caught at dev time
function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
getUser('abc').then(user => console.log(user.email));
// What if user is null? What if email doesn't exist? You find out in production.
// TypeScript: Catch it while you type
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'moderator';
}
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data: User = await response.json();
return data; // Type guaranteed!
}
// Now this won't even compile:
// getUser('abc'); // ❌ Argument of type 'string' not assignable to 'number'
// user.nonexistentProp; // ❌ Property does not exist on User
The Type System Essentials
// Primitives
let isActive: boolean = true;
let count: number = 42;
let name: string = "Alice";
let data: null = null;
let nothing: undefined = undefined;
// Arrays
const numbers: number[] = [1, 2, 3];
const names: Array<string> = ['Alice', 'Bob'];
// Tuple (fixed length, typed positions)
const coord: [number, number] = [10, 20];
const status: [number, string] = [200, "OK"];
// Objects
interface Product {
id: string;
name: string;
price: number;
tags?: string[]; // Optional property
readonly createdAt: Date; // Read-only
}
// Union types
type Status = 'pending' | 'active' | 'archived';
type ID = string | number;
// Literal types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
// Intersection types (combine multiple types)
type WithTimestamps<T> = T & { createdAt: Date; updatedAt: Date };
// Functions
function greet(name: string): string { return `Hello ${name}`; }
const add = (a: number, b: number): number => a + b;
// Optional parameters
function fetch(url: string, method?: HttpMethod = 'GET'): void {}
// Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
// Function overloads
function parseInput(value: string): string;
function parseInput(value: number): number;
function parseInput(value: string | number): string | number {
if (typeof value === 'string') return value.toUpperCase();
return value * 2;
}
Generics: Reusable Types
// Generic function — works with any type
function first<T>(items: T[]): T | undefined {
return items[0];
}
first([1, 2, 3]); // Returns number
first(['a', 'b']); // Returns string
// Generic interface
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: Date;
}
// Usage:
type UserResponse = ApiResponse<User>;
type ProductsResponse = ApiResponse<Product[]>;
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url);
return res.json(); // Type-safe response!
}
// Generic class
class Repository<T> {
private items: T[] = [];
add(item: T): void { this.items.push(item); }
findById(id: number): T | undefined { return this.items.find(i => i['id'] === id); }
findAll(): T[] { return [...this.items]; }
update(id: number, updates: Partial<T>): T | undefined {
const item = this.findById(id);
if (!item) return undefined;
Object.assign(item, updates);
return item;
}
}
const userRepo = new Repository<User>();
userRepo.add({ id: 1, name: 'Alice', email: 'alice@test.com', role: 'admin', createdAt: new Date() });
// Generic constraints
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 }, 'email'); // ❌ compile error!
Utility Types & Patterns
// Built-in utility types you'll use every day:
interface User {
id: number; name: string; email: string; role: string; password: string;
}
// Pick — select specific properties
type PublicUser = Pick<User, 'id' | 'name'>;
// { id: number; name: string }
// Omit — exclude properties
type SafeUser = Omit<User, 'password'>;
// { id: number; name: string; email: string; role: string }
// Partial — make all properties optional
type UpdateUserInput = Partial<User>;
// All fields optional — perfect for update forms!
// Required — make all properties required
type CompleteUser = Required<Partial<User>>;
// Record — create object type from keys
type RolePermissions = Record<string, string[]>;
// { admin: string[]; user: string[]; moderator: string[] }
// Extract — extract matching union members
type StringKeys = Extract<keyof User, string>; // All string keys
// ReturnType — get function's return type
type FetchResult = typeof fetch; // Function type
// Practical patterns:
// Discriminated unions (pattern matching!)
type Result<T> =
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function handleResult(result: Result<User>) {
switch (result.status) {
case 'success':
console.log(result.data.name); // TypeScript knows data exists here
break;
case 'error':
console.error(result.error.message); // Knows error exists here
break;
}
}
// Branded types (prevent mixing up similar primitives)
type UserId = string & { __brand: 'UserId' };
type OrderId = string & { __brand: 'OrderId' };
function toUserId(id: string): UserId { return id as UserId; }
function toOrderId(id: string): OrderId { return id as OrderId; }
function getUserById(id: UserId) {}
function getOrderById(id: OrderId) {}
getUserById(toUserId('123')); // ✅
getOrderById(toOrderId('456')); // ✅
// getUserById('123'); // ❌ Won't compile!
// Exhaustive checking (never forget a case)
type Action =
| { type: 'LOAD' }
| { type: 'SUCCESS'; payload: User[] }
| { type: 'ERROR'; message: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'LOAD': return { ...state, loading: true };
case 'SUCCESS': return { ...state, loading: false, users: action.payload };
case 'ERROR': return { ...state, loading: false, error: action.message };
default: return assertNever(action); // Compile error if new action added!
}
}
function assertNever(x: never): never { throw new Error(`Unexpected: ${x}`); }
tsconfig.json That Works
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
What's your favorite TypeScript tip? What confused you most when learning?
Follow @armorbreak for more practical developer guides.
Top comments (0)