DEV Community

Alex Chen
Alex Chen

Posted on

TypeScript Essentials: A Practical Guide for JS 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 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
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

What's your favorite TypeScript tip? What confused you most when learning?

Follow @armorbreak for more practical developer guides.

Top comments (0)