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 for catching bugs before they ship. Here's what you actually need to know.

Why TypeScript Matters

// ❌ JavaScript: Bugs hide until runtime
function calculateDiscount(price, isPremium) {
  return price * isPremium ? 0.8 : 1; // Operator precedence bug!
}
calculateDiscount(100, false); // Returns 0.8 instead of 100! 💥

// ✅ TypeScript: Catches it at compile time
// Error: can't multiply number by boolean implicitly
function calculateDiscount(price: number, isPremium: boolean): number {
  return price * (isPremium ? 0.8 : 1); // Fixed!
}
Enter fullscreen mode Exit fullscreen mode

Type System Essentials

// Primitive types
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"];
const mixed: (string | number)[] = [1, "two", 3];

// Objects
interface User {
  id: string;
  name: string;
  email?: string;           // Optional property
  readonly createdAt: Date; // Read-only
  role: 'admin' | 'editor' | 'viewer'; // Union literal type
}

const user: User = {
  id: crypto.randomUUID(),
  name: "Alice",
  createdAt: new Date(),
  role: "admin"
};

// Functions with full typing
function fetchData<T>(url: string): Promise<T> { /* ... */ }
type FetchHandler<T> = (data: T, error?: Error) => void;

async function fetchWithHandler<T>(
  url: string,
  handler: FetchHandler<T>
): Promise<void> {
  try {
    const response = await fetch(url);
    const data: T = await response.json();
    handler(data);
  } catch (err) {
    handler({} as T, err as Error);
  }
}

// Usage:
fetchWithHandler<User[]>('/api/users', (users, err) => {
  if (err) console.error(err);
  else users.forEach(u => console.log(u.name));
});
Enter fullscreen mode Exit fullscreen mode

Advanced Types You'll Use Every Day

// Utility types (built-in):
interface Article {
  title: string;
  body: string;
  author: User;
  tags: string[];
  publishedAt: Date;
}

// Partial — make all properties optional
function updateArticle(id: string, fields: Partial<Article>): Article { /* ... */ }
updateArticle("abc", { title: "New Title" }); // Only update title

// Required — make all properties required
type CompleteArticle = Required<Article>;

// Pick — select specific properties
type ArticlePreview = Pick<Article, 'title' | 'author' | 'publishedAt'>;

// Omit — exclude specific properties
type UnsavedArticle = Omit<Article, 'id' | 'createdAt'>;

// Record — key-value map
type RolePermissions = Record<string, string[]>;
const permissions: RolePermissions = {
  admin: ['read', 'write', 'delete'],
  editor: ['read', 'write'],
  viewer: ['read']
};

// Custom utility types
type NonNullableFields<T> = { [K in keyof T]: NonNullable<T[K]> };
type Writable<T> = { -readonly [K in keyof T]: T[K] };

// Conditional types
type IsString<T> = T extends string ? true : false;
type Result = Isstring<string>; // true

// Mapped types
type ReadonlyDeep<T> = {
  readonly [P in keyof T]: T[P] extends object ? ReadonlyDeep<T[P]> : T[P]
};

// Template literal types
type EventName = `on${Capitalize<string>}`;
type ClickEvent = 'onClick'; // Valid
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiRoute<Method extends HttpMethod, Path extends string> =
  `${Method} ${Path}`;
Enter fullscreen mode Exit fullscreen mode

Real-World Patterns

// Pattern 1: Discriminated unions (type-safe state machines)
type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function handleState<T>(state: RequestState<T>) {
  switch (state.status) {
    case 'idle': return 'Not started';
    case 'loading': return 'Loading...';
    case 'success': return `Got ${state.data}`; // TS knows data exists here!
    case 'error': return `Error: ${state.error.message}`; // And error here!
  }
}

// Pattern 2: Branded types (prevent mixing similar values)
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<number, 'UserId'>;
type OrderId = Brand<number, 'OrderId'>;

function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ */

const uid: UserId = 123 as UserId; // Must explicitly brand
const oid: OrderId = 456 as OrderId;

getUser(uid);  // ✅
getUser(oid);  // ❌ Error: OrderId not assignable to UserId!

// Pattern 3: Type-safe API client
class ApiClient {
  async get<T>(path: string): Promise<T> {
    const res = await fetch(path);
    if (!res.ok) throw new ApiError(res.status, res.statusText);
    return res.json() as Promise<T>;
  }

  async post<T>(path: string, body: unknown): Promise<T> {
    const res = await fetch(path, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body)
    });
    if (!res.ok) throw new ApiError(res.status, res.statusText);
    return res.json() as Promise<T>;
  }
}

// Type-safe route definitions:
const api = new ApiClient();
const users = await api.get<User[]>('/api/users');
const user = await api.post<User>('/api/users', { name: 'Bob' });

// Pattern 4: Zod + TypeScript (runtime validation)
import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(100),
  email: z.string().email(),
  role: z.enum(['admin', 'editor', 'viewer'])
});

type UserFromSchema = z.infer<typeof UserSchema>; // Auto-derived type!

function validateUser(data: unknown): UserFromSchema {
  return UserSchema.parse(data); // Throws on invalid data
}

// Pattern 5: Generic components (React example)
interface Props<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
  emptyMessage?: string;
}

function List<T>({ items, renderItem, keyExtractor, emptyMessage = 'No items' }: Props<T>) {
  if (items.length === 0) return <p>{emptyMessage}</p>;
  return <ul>{items.map(item => <li key={keyExtractor(item)}>{renderItem(item)}</li>)}</ul>;
}

// Usage:
<List users={users} keyExtractor={u => u.id} renderItem={u => <span>{u.name}</span>} />
Enter fullscreen mode Exit fullscreen mode

Migration Strategy

# Step 1: Add TypeScript to existing JS project
npm install --save-dev typescript @types/node @types/express
npx tsc --init

# tsconfig.json essentials:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
Enter fullscreen mode Exit fullscreen mode
// Migration path:
// 1. Rename .js → .ts (start with leaf files, no imports)
// 2. Add @ts-expect-error for things you'll fix later
// 3. Add types to function signatures first (biggest bang for buck)
// 4. Gradually enable stricter compiler options
// 5. Add JSDoc @types for .js files you haven't converted yet

// Quick wins — add types to these first:
// - Function parameters and return types
// - API request/response shapes
// - Database query results
// - Environment variables
Enter fullscreen mode Exit fullscreen mode

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

Follow @armorbreak for more practical developer guides.

Top comments (0)