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

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

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

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

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

What's your favorite TypeScript feature? What confused you when you started?

Follow @armorbreak for more practical developer guides.

Top comments (0)