DEV Community

Alex Chen
Alex Chen

Posted on

TypeScript: The Practical Guide for JavaScript Developers (2026)

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 }'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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'] }
Enter fullscreen mode Exit fullscreen mode

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)