TypeScript: The Practical Guide for JavaScript Developers (2026)
TypeScript isn't just "JavaScript with types" — it's a superpower. Here's how to actually use it effectively.
Why TypeScript Matters (In 2026)
// The problem TypeScript solves:
// JavaScript errors that only show up at RUNTIME:
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// This compiles fine but CRASHES at runtime:
calculateTotal(null); // TypeError: Cannot read property 'reduce' of null
calculateTotal("not an array"); // TypeError: items.reduce is not a function
calculateTotal([{ price: 10 }, { cost: 20 }]); // NaN! (wrong property name)
// With TypeScript — caught at COMPILE time:
interface CartItem {
id: string;
name: string;
price: number;
quantity?: number; // Optional field
}
function calculateTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price * (item.quantity ?? 1), 0);
}
calculateTotal(null); // Error: Argument of type 'null' is not assignable
calculateTotal("not array"); // Error: Type 'string' is not assignable to type 'CartItem[]'
calculateTotal([{ cost: 20 }]); // Error: Property 'price' is missing in type '{ cost: number }'
The Essentials You Need Day One
// === Basic Types ===
let name: string = "Alice";
let age: number = 30;
let isActive: boolean = true;
let data: any = "anything goes"; // Avoid when possible!
let unknown: unknown = "safer than any"; // Must check before using
let nothing: null = null;
let missing: undefined = undefined;
// === Arrays ===
const numbers: number[] = [1, 2, 3];
const names: Array<string> = ["Alice", "Bob"]; // Same thing, generic syntax
// Read-only arrays (prevent accidental mutation):
const config: readonly string[] = ["dev", "staging", "prod"];
config.push("test"); // Error: Property 'push' does not exist on readonly string[]
// Tuples (fixed-length, typed arrays):
type HTTPResponse = [number, string, Record<string, unknown>];
const response: HTTPResponse = [200, "OK", { data: [] }];
const [status, message, body] = response; // Destructure with correct types!
// === Objects & Interfaces ===
// Interface (best for object shapes):
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'editor' | 'viewer'; // Union literal type
createdAt: Date;
preferences?: { // Optional nested object
theme: 'light' | 'dark';
notifications: boolean;
};
}
// Type alias (more flexible, can do unions/intersections):
type ID = string | number;
type Status = 'pending' | 'active' | 'archived';
type UserWithStatus = User & { status: Status }; // Intersection!
// When to use which:
// interface → object shapes, class implementations, extendable
// type → unions, intersections, computed/mapped types, complex combinations
// === Functions ===
function greet(name: string): string {
return `Hello, ${name}!`;
}
// Arrow function:
const double = (n: number): number => n * 2;
// Optional parameters:
function createUser(name: string, age?: number): User {
return { id: crypto.randomUUID(), name, email: '', role: 'viewer', createdAt: new Date() };
}
// Default parameters:
function formatCurrency(amount: number, currency: string = 'USD'): string {
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}
// Rest parameters:
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4, 5); // 15
// Function overloads (different behavior based on input type):
function parseInput(input: string): string;
function parseInput(input: number): number;
function parseInput(input: string | number): string | number {
if (typeof input === 'string') return input.toUpperCase();
return input * 2;
}
parseInput("hello"); // "HELLO"
parseInput(42); // 84
Generics: Reusable, Type-Safe Code
// The problem: You want a function that works with ANY type but preserves type info:
function first(arr: any[]) { return arr[0]; }
const result = first([1, 2, 3]); // Type is 'any' — lost the information!
// Solution: Generics
function first<T>(arr: T[]): T | undefined {
return arr[0]; // Preserves whatever type T is
}
const num = first([1, 2, 3]); // Type: number | undefined ✅
const str = first(["a", "b"]); // Type: string | undefined ✅
const empty = first([]); // Type: undefined ✅
// Generic interfaces:
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(); // Type-checked!
}
async function fetchPosts(): Promise<ApiResponse<Post[]>> {
const res = await fetch('/api/posts');
return res.json(); // Same function shape, different data type!
// Generic constraints (restrict what T can be):
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
// T must have an 'id' property of type string
// Multiple type parameters:
function merge<K, V>(keys: K[], values: V[]): Map<K, V> {
const map = new Map<K, V>();
keys.forEach((k, i) => map.set(k, values[i]));
return map;
}
Utility Types That Save Time
// Partial<T> — Make all properties optional:
interface Product {
name: string;
price: number;
category: string;
inStock: boolean;
}
function updateProduct(id: string, fields: Partial<Product>): Product {
const existing = db.products.find(id);
return { ...existing, ...fields }; // Only update provided fields
}
updateProduct("abc", { price: 29.99 }); // Only need to pass what's changing!
// Required<T> — Make all properties required:
type CompleteUser = Required<User>; // Even optional fields become required
// Pick<T, K> — Select specific properties:
type ProductPreview = Pick<Product, 'name' | 'price'>;
// { name: string; price: number; }
// Omit<T, K> — Exclude specific properties:
type PublicProduct = Omit<Product, 'inStock'>;
// { name: string; price: string; category: string; }
// Record<K, V> — Dictionary/object with typed keys and values:
type RolePermissions = Record<User['role'], string[]>;
const permissions: RolePermissions = {
admin: ['*'],
editor: ['read', 'write', 'publish'],
viewer: ['read'],
};
// ReturnType<T> — Get return type of a function:
type FetchResult = typeof fetchUser; // Function type
type UserData = ReturnType<typeof fetchUser>; // Promise<ApiResponse<User>>
// Parameters<T> — Get parameter types as tuple:
type GreetParams = Parameters<typeof greet>; // [string]
// NonNullable<T> — Remove null and undefined:
type SafeString = NonNullable<string | null | undefined>; // string
// Awaited<T> — Unwrap Promise type:
type ResolvedData = Awaited<Promise<ApiResponse<User>>>; // ApiResponse<User>
Practical Patterns for Real Projects
// 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[]>): string {
switch (state.status) {
case 'idle': return 'Enter a search query';
case 'loading': return 'Loading...';
case 'success': return `Found ${state.data.length} users`;
case 'error': return `Error: ${state.error.message}`;
// No default needed — TypeScript knows all cases are covered!
}
}
// Pattern 2: Branded types (prevent mixing up similar primitives):
type Brand<T, B extends string> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type SessionId = Brand<string, 'SessionId'>;
function createUserId(id: string): UserId { return id as UserId; }
function createSessionId(id: string): SessionId { return id as SessionId; }
function getUser(uid: UserId) { /* ... */ }
function validateSession(sid: SessionId) { /* ... */ */
const uid = createUserId('user-123');
const sid = createSessionId('sess-456');
getUser(uid); // ✅ Works
getUser(sid); // ❌ Error: Argument of type 'SessionId' is not assignable to parameter of type 'UserId'
// Pattern 3: Type guards (runtime type checking):
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isArray<T>(value: unknown): value is T[] {
return Array.isArray(value);
}
// Custom type guard:
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// Usage:
function processInput(input: unknown) {
if (isUser(input)) {
console.log(input.name); // TypeScript knows it's a User here!
}
}
// Pattern 4: const assertions (most specific types possible):
const CONFIG = {
API_URL: 'https://api.example.com',
MAX_RETRIES: 3,
FEATURES: ['auth', 'billing', 'analytics'] as const,
} as const;
// Type is exactly:
// { readonly API_URL: 'https://api.example.com'; readonly MAX_RETRIES: 3; readonly FEATURES: readonly ['auth', 'billing', 'analytics'] }
What's your favorite TypeScript feature? What TS concept took you way too long to understand?
Follow @armorbreak for more practical developer guides.
Top comments (0)