Skip to main content
πŸ“ Technical Article

Getting Started with TypeScript: From JavaScript to Type Safety in 2025

Billie Heidelberg Jr.
Billie Heidelberg Jr.
Full Stack Developer
35 min read
Cover image for Getting Started with TypeScript: From JavaScript to Type Safety in 2025

Getting Started with TypeScript: From JavaScript to Type Safety in 2025

Tired of runtime errors that could have been caught earlier? Ready to level up your JavaScript with superpowers? TypeScript is your gateway to more reliable, maintainable code that scales with your projects. This comprehensive guide will take you from TypeScript-zero to TypeScript-hero with practical examples and real-world patterns.

🎯 What You'll Learn

By the end of this guide, you'll master:

  • What TypeScript is and why it matters
  • Setting up your TypeScript development environment
  • Core type system fundamentals
  • Practical typing patterns for everyday development
  • Common pitfalls and how to avoid them
  • Real-world project integration
  • When TypeScript shines vs. when to stick with JavaScript

πŸš€ The Lightning-Fast Overview

TypeScript follows a simple concept:

// JavaScript + Types = TypeScript
const user = { name: "John", age: 30 }; // JavaScript
const user: User = { name: "John", age: 30 }; // TypeScript

That's it! Everything else builds on this foundation.

πŸ“š Prerequisites

Before diving in, make sure you're comfortable with:

βœ… JavaScript fundamentals (variables, functions, objects, arrays) βœ… ES6+ features (arrow functions, destructuring, modules) βœ… Basic command line usage βœ… Node.js and npm basics

New to JavaScript? Check out MDN's JavaScript Guide first.

πŸ“‹ Table of Contents

  1. Why TypeScript Matters
  2. Setting Up Your Environment
  3. Type System Fundamentals
  4. Practical Typing Patterns
  5. Working with Objects & Functions
  6. Advanced Types Made Simple
  7. Real-World Integration
  8. Common Pitfalls & Solutions
  9. TypeScript vs JavaScript: When to Use What

Why TypeScript Matters

The JavaScript Problem

JavaScript is incredibly flexible, but that flexibility comes with a cost:

// JavaScript - These errors only show up at runtime
function calculateTotal(price, tax) {
  return price + tax; // What if price is a string?
}

calculateTotal("50", 0.08); // Returns "500.08" instead of 50.08!
calculateTotal(50); // Returns NaN because tax is undefined

// Accessing properties that might not exist
const user = getUser();
console.log(user.profile.settings.theme); // Could crash if profile is null

The TypeScript Solution

TypeScript catches these issues before your code runs:

// TypeScript - Errors caught at compile time
function calculateTotal(price: number, tax: number): number {
  return price + tax;
}

calculateTotal("50", 0.08); // ❌ Compile error: Argument of type 'string' is not assignable to parameter of type 'number'
calculateTotal(50); // ❌ Compile error: Expected 2 arguments, but got 1

// Safe property access
interface User {
  profile?: {
    settings?: {
      theme: string;
    };
  };
}

const user: User = getUser();
console.log(user.profile?.settings?.theme); // Safe with optional chaining

Real-World Benefits

  • Catch bugs early: 80% of runtime errors can be prevented at compile time
  • Better IntelliSense: Your editor knows exactly what properties and methods are available
  • Refactoring confidence: Rename a property and TypeScript finds all usages
  • Self-documenting code: Types serve as inline documentation
  • Team collaboration: Clear contracts between different parts of your application

Setting Up Your Environment

Quick Start (5 minutes)

The fastest way to try TypeScript:

# Install TypeScript globally
npm install -g typescript

# Create a new project
mkdir my-typescript-project
cd my-typescript-project
npm init -y

# Install TypeScript for this project
npm install -D typescript @types/node

# Initialize TypeScript config
npx tsc --init

# Create your first TypeScript file
echo 'console.log("Hello, TypeScript!");' > index.ts

# Compile and run
npx tsc index.ts
node index.js

Professional Setup

For real projects, use this setup:

# Create project structure
mkdir typescript-project
cd typescript-project

# Initialize package.json
npm init -y

# Install dependencies
npm install -D typescript @types/node ts-node nodemon
npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser

# Create TypeScript config
npx tsc --init

Create a professional tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Add these scripts to package.json:

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node src/index.ts",
    "watch": "nodemon --exec ts-node src/index.ts"
  }
}

VS Code Setup

Install these essential extensions:

  • TypeScript Importer - Auto import suggestions
  • Error Lens - Inline error highlighting
  • TypeScript Hero - Additional TypeScript tools
  • Prettier - Code formatting

Type System Fundamentals

TypeScript Type Hierarchy: A Visual Guide

Understanding how types relate to each other is crucial for mastering TypeScript. Here's a visual representation of the TypeScript type system:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         any & unknown                        β”‚
β”‚                              β–²                              β”‚
β”‚                              β”‚                              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚          β”‚          β”‚        β”‚        β”‚          β”‚         β”‚
β”‚  string  β”‚  number  β”‚ booleanβ”‚  objectβ”‚  array   β”‚ functionβ”‚
β”‚          β”‚          β”‚        β”‚        β”‚          β”‚         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                              β”‚                              β”‚
β”‚                            null                            β”‚
β”‚                          undefined                         β”‚
β”‚                                                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Type Relationships                      β”‚
β”‚                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                                β”‚
β”‚  β”‚ string  │◄────┐                                         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚                                         β”‚
β”‚                  β”‚                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚                                         β”‚
β”‚  β”‚ number  │◄─────                                         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚                                         β”‚
β”‚                  β”œβ”€β”€β”€β”€ Union Types (string | number)        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚                                         β”‚
β”‚  β”‚ boolean │◄─────                                         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚                                         β”‚
β”‚                  β”‚                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚                                         β”‚
β”‚  β”‚ literal β”‚β—„β”€β”€β”€β”€β”˜                                         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                               β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key concepts illustrated:

  • any and unknown are at the top of the hierarchy (least type-safe)
  • Primitive types form the foundation of the system
  • null and undefined are special bottom types
  • Union types combine multiple types
  • Literal types are specific values of a primitive type

Basic Types

TypeScript includes all JavaScript types plus additional ones:

// Primitive types
let name: string = "John";
let age: number = 30;
let isActive: boolean = true;
let data: null = null;
let notDefined: undefined = undefined;

// Arrays
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ["a", "b", "c"];

// Objects
let user: {
  name: string;
  age: number;
} = {
  name: "John",
  age: 30
};

// Functions
function greet(name: string): string {
  return `Hello, ${name}!`;
}

// Arrow functions
const add = (a: number, b: number): number => a + b;

Type Inference

TypeScript is smart about inferring types:

// TypeScript infers the types automatically
let message = "Hello"; // inferred as string
let count = 42; // inferred as number
let items = [1, 2, 3]; // inferred as number[]

// You can still be explicit when needed
let explicitMessage: string = "Hello";

// Best practice: Let TypeScript infer when obvious
let user = {
  name: "John", // string
  age: 30, // number
  active: true // boolean
}; // inferred as { name: string; age: number; active: boolean; }

Union Types

Handle multiple possible types:

// Union types with |
let id: string | number;
id = "abc123"; // βœ… Valid
id = 123; // βœ… Valid
id = true; // ❌ Error

// Practical example
function formatId(id: string | number): string {
  if (typeof id === "string") {
    return id.toUpperCase(); // TypeScript knows id is string here
  }
  return id.toString(); // TypeScript knows id is number here
}

// Array with mixed types
let mixedArray: (string | number)[] = ["hello", 42, "world", 123];

Literal Types

Be specific about exact values:

// String literals
let direction: "up" | "down" | "left" | "right";
direction = "up"; // βœ… Valid
direction = "diagonal"; // ❌ Error

// Number literals
let dice: 1 | 2 | 3 | 4 | 5 | 6;

// Boolean literals (less common but possible)
let success: true = true; // Can only be true, not false

// Practical example: HTTP methods
function apiCall(method: "GET" | "POST" | "PUT" | "DELETE", url: string) {
  // Implementation here
}

Practical Typing Patterns

Interfaces vs Types

Both define object shapes, but with subtle differences:

// Interface - prefer for object shapes
interface User {
  id: number;
  name: string;
  email: string;
  isActive?: boolean; // Optional property
}

// Type alias - good for unions and computed types
type Status = "pending" | "approved" | "rejected";
type ID = string | number;

// Interfaces can be extended
interface AdminUser extends User {
  permissions: string[];
  lastLogin: Date;
}

// Types can use computed properties
type UserKeys = keyof User; // "id" | "name" | "email" | "isActive"

Optional vs Required Properties

interface CreateUserRequest {
  name: string; // Required
  email: string; // Required
  age?: number; // Optional
  bio?: string; // Optional
}

function createUser(userData: CreateUserRequest): User {
  return {
    id: Math.random(),
    name: userData.name,
    email: userData.email,
    isActive: true,
    // age and bio are optional
    ...(userData.age && { age: userData.age }),
    ...(userData.bio && { bio: userData.bio })
  };
}

// Usage
createUser({ name: "John", email: "john@example.com" }); // βœ… Valid
createUser({ name: "John" }); // ❌ Error: email is required

Readonly Properties

Prevent accidental mutations:

interface Config {
  readonly apiUrl: string;
  readonly version: string;
  settings: {
    readonly theme: string;
    notifications: boolean; // Can be changed
  };
}

const config: Config = {
  apiUrl: "https://api.example.com",
  version: "1.0.0",
  settings: {
    theme: "dark",
    notifications: true
  }
};

config.apiUrl = "https://new-api.com"; // ❌ Error: Cannot assign to readonly property
config.settings.notifications = false; // βœ… Valid: notifications is not readonly

Working with Objects & Functions

Function Signatures

// Basic function types
type CalculatorFunction = (a: number, b: number) => number;

const add: CalculatorFunction = (a, b) => a + b;
const multiply: CalculatorFunction = (a, b) => a * b;

// Functions with optional parameters
function greetUser(name: string, greeting?: string): string {
  return `${greeting || "Hello"}, ${name}!`;
}

// Functions with default parameters
function createUser(name: string, role: string = "user"): User {
  return { id: Math.random(), name, role };
}

// Rest parameters
function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}

Function Overloads

Handle different parameter combinations:

// Function overloads
function getValue(key: string): string;
function getValue(key: string, defaultValue: string): string;
function getValue(key: string, defaultValue?: string): string {
  const value = localStorage.getItem(key);
  return value !== null ? value : (defaultValue || "");
}

// Usage
const theme = getValue("theme"); // Returns string
const language = getValue("language", "en"); // Returns string with default

Generic Functions

Create reusable functions that work with multiple types:

// Generic function
function getFirstItem<T>(items: T[]): T | undefined {
  return items[0];
}

// Usage with type inference
const firstNumber = getFirstItem([1, 2, 3]); // number | undefined
const firstString = getFirstItem(["a", "b", "c"]); // string | undefined
const firstUser = getFirstItem([user1, user2]); // User | undefined

// Generic with constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "John", age: 30 };
const userName = getProperty(user, "name"); // string
const userAge = getProperty(user, "age"); // number

Advanced Types Made Simple

Utility Types

TypeScript provides helpful utility types:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// Partial - makes all properties optional
type UserUpdate = Partial<User>;
const updateData: UserUpdate = { name: "New Name" }; // Only name is provided

// Pick - select specific properties
type UserProfile = Pick<User, "id" | "name" | "email">;
const profile: UserProfile = { id: 1, name: "John", email: "john@example.com" };

// Omit - exclude specific properties
type CreateUser = Omit<User, "id">;
const newUser: CreateUser = { name: "John", email: "john@example.com", password: "secret" };

// Required - makes all properties required
type RequiredUser = Required<User>;

// Record - create object type with specific keys and values
type UserRoles = Record<string, string[]>;
const roles: UserRoles = {
  admin: ["read", "write", "delete"],
  user: ["read"],
  guest: []
};

Conditional Types

Types that change based on conditions:

// Simple conditional type
type IsString<T> = T extends string ? true : false;

type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false

// Practical example: API response types
type ApiResponse<T> = T extends string 
  ? { message: T } 
  : { data: T };

type StringResponse = ApiResponse<string>; // { message: string }
type UserResponse = ApiResponse<User>; // { data: User }

Mapped Types

Transform existing types:

// Make all properties optional
type Optional<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties readonly
type ReadOnly<T> = {
  readonly [K in keyof T]: T[K];
};

// Transform property types
type Stringify<T> = {
  [K in keyof T]: string;
};

type UserStrings = Stringify<User>; 
// { id: string; name: string; email: string; password: string; }

Real-World Integration

React with TypeScript

import React, { useState } from 'react';

// Component props interface
interface ButtonProps {
  children: React.ReactNode;
  onClick: () => void;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
}

// Functional component with TypeScript
const Button: React.FC<ButtonProps> = ({ 
  children, 
  onClick, 
  variant = 'primary', 
  disabled = false 
}) => {
  return (
    <button 
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
};

// Hook with TypeScript
function useCounter(initialValue: number = 0) {
  const [count, setCount] = useState<number>(initialValue);
  
  const increment = () => setCount(prev => prev + 1);
  const decrement = () => setCount(prev => prev - 1);
  const reset = () => setCount(initialValue);
  
  return { count, increment, decrement, reset };
}

Express API with TypeScript

import express, { Request, Response } from 'express';

// Request/Response type extensions
interface AuthenticatedRequest extends Request {
  user?: User;
}

interface CreateUserBody {
  name: string;
  email: string;
}

// Route handler with types
const createUser = async (
  req: Request<{}, User, CreateUserBody>, 
  res: Response<User | { error: string }>
) => {
  try {
    const { name, email } = req.body;
    
    // Validation
    if (!name || !email) {
      return res.status(400).json({ error: 'Name and email are required' });
    }
    
    const user = await userService.create({ name, email });
    res.status(201).json(user);
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
};

// Type-safe route registration
app.post('/users', createUser);

Working with APIs

// API response types
interface ApiUser {
  id: number;
  name: string;
  email: string;
  created_at: string;
}

interface ApiResponse<T> {
  data: T;
  status: 'success' | 'error';
  message?: string;
}

// Generic API client
class ApiClient {
  private baseUrl: string;
  
  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }
  
  async get<T>(endpoint: string): Promise<ApiResponse<T>> {
    const response = await fetch(`${this.baseUrl}${endpoint}`);
    return response.json();
  }
  
  async post<T, U>(endpoint: string, data: T): Promise<ApiResponse<U>> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    return response.json();
  }
}

// Usage
const api = new ApiClient('https://api.example.com');

const getUsers = async (): Promise<ApiUser[]> => {
  const response = await api.get<ApiUser[]>('/users');
  return response.data;
};

Common Pitfalls & Solutions

❌ Pitfall 1: Using any Everywhere

// ❌ Don't do this
function processData(data: any): any {
  return data.someProperty.whatever;
}

// βœ… Use specific types
interface ProcessableData {
  someProperty: {
    whatever: string;
  };
}

function processData(data: ProcessableData): string {
  return data.someProperty.whatever;
}

// βœ… Or use generics when you need flexibility
function processData<T extends { someProperty: unknown }>(data: T): T['someProperty'] {
  return data.someProperty;
}

❌ Pitfall 2: Ignoring Null/Undefined

// ❌ Dangerous
function getUserName(user: User): string {
  return user.name.toUpperCase(); // Could crash if name is null
}

// βœ… Handle null/undefined explicitly
function getUserName(user: User): string {
  if (!user.name) {
    return 'Unknown User';
  }
  return user.name.toUpperCase();
}

// βœ… Or use optional chaining and nullish coalescing
function getUserName(user: User): string {
  return user.name?.toUpperCase() ?? 'Unknown User';
}

❌ Pitfall 3: Over-Complex Types

// ❌ Too complex
type SuperComplexType<T, U, V> = T extends string 
  ? U extends number 
    ? V extends boolean 
      ? string 
      : number 
    : boolean 
  : never;

// βœ… Keep it simple and readable
type UserRole = 'admin' | 'user' | 'guest';
type UserPermission = 'read' | 'write' | 'delete';

interface UserAccess {
  role: UserRole;
  permissions: UserPermission[];
}

❌ Pitfall 4: Not Using Type Guards

// ❌ Type assertion without checking
function processValue(value: unknown): string {
  return (value as string).toUpperCase(); // Dangerous!
}

// βœ… Use type guards
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function processValue(value: unknown): string {
  if (isString(value)) {
    return value.toUpperCase(); // Safe!
  }
  throw new Error('Value is not a string');
}

Troubleshooting Common TypeScript Errors

Visual Guide to TypeScript Error Messages

TypeScript error messages can be intimidating at first. Here's how to decode and fix the most common ones:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ERROR TS2322: Type 'string' is not assignable to type 'number'.
β”‚                  ↑           ↑                      ↑
β”‚                  β”‚           β”‚                      β”‚
β”‚                  β”‚           β”‚                      └─ Expected type
β”‚                  β”‚           └─ Error message
β”‚                  └─ Actual type
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

1. Type Assignment Errors

Error: TS2322: Type 'X' is not assignable to type 'Y'

Common causes:

  • Trying to assign incompatible types
  • Forgetting to handle all possible types in a union
  • Library type definitions don't match actual usage

Solutions:

// Problem
const id: number = "123"; // Error: Type 'string' is not assignable to type 'number'

// Solutions

// 1. Fix the type
const id: number = 123;

// 2. Convert the value
const id: number = parseInt("123", 10);

// 3. Use a union type if both are valid
const id: string | number = "123";

2. Object Property Errors

Error: TS2339: Property 'X' does not exist on type 'Y'

Common causes:

  • Typos in property names
  • Accessing properties that might not exist
  • Using properties before they're defined

Solutions:

// Problem
interface User {
  name: string;
  email: string;
}

const user: User = { name: "John", email: "john@example.com" };
console.log(user.phone); // Error: Property 'phone' does not exist on type 'User'

// Solutions

// 1. Update the interface
interface User {
  name: string;
  email: string;
  phone?: string; // Optional property
}

// 2. Use optional chaining
console.log(user?.phone); // undefined (no error)

// 3. Use type assertion (only when you're certain)
console.log((user as any).phone); // undefined (avoid if possible)

3. Function Parameter Errors

Error: TS2554: Expected X arguments, but got Y

Common causes:

  • Missing required parameters
  • Providing too many arguments
  • Incorrect parameter order

Solutions:

// Problem
function greet(name: string, greeting: string): string {
  return `${greeting}, ${name}!`;
}

greet("John"); // Error: Expected 2 arguments, but got 1

// Solutions

// 1. Provide all required arguments
greet("John", "Hello");

// 2. Make parameters optional
function greet(name: string, greeting?: string): string {
  return `${greeting || "Hello"}, ${name}!`;
}

// 3. Use default parameters
function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

4. Type Narrowing Errors

Error: TS2339: Property 'X' does not exist on type 'Y | Z'

Common causes:

  • Not narrowing union types before accessing specific properties
  • Missing type guards

Solutions:

// Problem
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; sideLength: number };

type Shape = Circle | Square;

function getArea(shape: Shape): number {
  return Math.PI * shape.radius * shape.radius; // Error: Property 'radius' does not exist on type 'Shape'
}

// Solutions

// 1. Use type guards with discriminated unions
function getArea(shape: Shape): number {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius * shape.radius; // TypeScript knows shape is Circle here
  } else {
    return shape.sideLength * shape.sideLength; // TypeScript knows shape is Square here
  }
}

// 2. Use type assertion (only when you're certain)
function getCircleArea(shape: Shape): number {
  return Math.PI * (shape as Circle).radius * (shape as Circle).radius;
}

5. Library and Module Errors

Error: TS2307: Cannot find module 'X' or its corresponding type declarations

Common causes:

  • Missing npm package
  • Missing type definitions
  • Incorrect import path

Solutions:

# Install the missing package
npm install package-name

# Install type definitions
npm install --save-dev @types/package-name
// Create custom type declarations for modules without types
// declarations.d.ts
declare module 'untyped-module' {
  export function doSomething(): void;
  // Add other exports as needed
}

6. Debugging Complex Type Errors

For complex type issues, use these techniques:

// 1. Use type annotations to see what TypeScript infers
type Debug<T> = { [K in keyof T]: T[K] };
type Result = Debug<ComplexType>; // Hover over Result to see expanded type

// 2. Use the 'typeof' operator to check runtime types
const data = fetchData();
console.log(typeof data); // "object", "string", etc.

// 3. Use type predicates for custom type guards
function isValidResponse(response: unknown): response is ApiResponse {
  return (
    typeof response === "object" &&
    response !== null &&
    "data" in response &&
    "status" in response
  );
}

7. Configuration Errors

Common issues:

  • Incorrect tsconfig.json settings
  • Incompatible library versions
  • Missing type definitions

Solutions:

// tsconfig.json fixes for common issues
{
  "compilerOptions": {
    // Fix "Cannot use JSX unless the '--jsx' flag is provided"
    "jsx": "react",
    
    // Fix "Cannot find name 'document'"
    "lib": ["dom", "dom.iterable", "esnext"],
    
    // Fix "Property 'x' does not exist on type 'y'"
    "strictNullChecks": false, // (use with caution!)
    
    // Fix "Cannot find module 'x'"
    "moduleResolution": "node",
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

TypeScript vs JavaScript: When to Use What

Choose TypeScript When:

βœ… Building applications (not just scripts) βœ… Working in teams (3+ developers) βœ… Long-term maintenance is important βœ… Complex business logic with many edge cases βœ… API integrations with specific data contracts βœ… You want better IDE support and refactoring

Stick with JavaScript When:

🟑 Rapid prototyping or proof of concepts 🟑 Simple scripts or one-off utilities 🟑 Learning new concepts (reduce cognitive load) 🟑 Legacy codebases where migration cost is too high 🟑 Team lacks TypeScript experience and timeline is tight

Migration Strategy

// Step 1: Start with .ts files and basic types
interface User {
  name: string;
  email: string;
}

// Step 2: Add type annotations gradually
function createUser(userData: Partial<User>): User {
  // Implementation
}

// Step 3: Enable strict mode incrementally
// tsconfig.json
{
  "compilerOptions": {
    "strict": false, // Start here
    "noImplicitAny": true, // Add this first
    "strictNullChecks": true, // Then this
    "strict": true // Finally enable all
  }
}

🎯 Try This: Build a Todo App

Let's put everything together with a practical example:

// types.ts
export interface Todo {
  id: string;
  title: string;
  completed: boolean;
  createdAt: Date;
}

export type TodoFilter = 'all' | 'active' | 'completed';

export interface TodoStats {
  total: number;
  completed: number;
  active: number;
}

// todoService.ts
class TodoService {
  private todos: Todo[] = [];

  create(title: string): Todo {
    const todo: Todo = {
      id: crypto.randomUUID(),
      title,
      completed: false,
      createdAt: new Date()
    };
    this.todos.push(todo);
    return todo;
  }

  getAll(): Todo[] {
    return [...this.todos];
  }

  getByFilter(filter: TodoFilter): Todo[] {
    switch (filter) {
      case 'active':
        return this.todos.filter(todo => !todo.completed);
      case 'completed':
        return this.todos.filter(todo => todo.completed);
      default:
        return this.todos;
    }
  }

  toggle(id: string): boolean {
    const todo = this.todos.find(t => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
      return true;
    }
    return false;
  }

  delete(id: string): boolean {
    const index = this.todos.findIndex(t => t.id === id);
    if (index > -1) {
      this.todos.splice(index, 1);
      return true;
    }
    return false;
  }

  getStats(): TodoStats {
    const total = this.todos.length;
    const completed = this.todos.filter(t => t.completed).length;
    return {
      total,
      completed,
      active: total - completed
    };
  }
}

// Usage
const todoService = new TodoService();

todoService.create("Learn TypeScript");
todoService.create("Build a project");

const stats = todoService.getStats();
console.log(`You have ${stats.active} active todos`);

πŸš€ What's Next?

Now that you understand TypeScript basics, here's your learning path:

Immediate Next Steps:

  • Practice: Convert a small JavaScript project to TypeScript
  • Experiment: Try building the todo app from this guide
  • Read: Explore the TypeScript handbook

Intermediate Challenges:

  • Advanced types: Dive deeper## Migration Strategies

Real-World Migration Case Studies

Moving from JavaScript to TypeScript doesn't have to happen all at once. Let's look at some real-world migration strategies with concrete examples:

Case Study 1: Large React Application Migration

Company: E-commerce platform with 200+ React components Challenge: Migrate without disrupting ongoing feature development

Strategy & Implementation:

  1. Created a migration roadmap:

    Phase 1: Infrastructure setup (2 weeks)
    Phase 2: Core utilities & services (4 weeks)
    Phase 3: Shared components (6 weeks)
    Phase 4: Feature-specific components (ongoing)
    
  2. Set up dual JavaScript/TypeScript compilation:

    // tsconfig.json
    {
      "compilerOptions": {
        "allowJs": true,
        "checkJs": false,
        "jsx": "react",
        // Other options...
      },
      "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"]
    }
    
  3. Prioritized high-value, low-risk components first:

    • Utility functions (date formatting, string manipulation)
    • API service layers
    • State management stores
  4. Results:

    • 40% reduction in runtime errors after 3 months
    • Improved developer onboarding time from 2 weeks to 1 week
    • Better IDE support for refactoring

Case Study 2: Express API Migration

Company: SaaS platform with Node.js/Express backend Challenge: Add type safety without rewriting entire codebase

Strategy & Implementation:

  1. Started with API boundaries:

    // Before: routes/users.js
    router.get('/users/:id', (req, res) => {
      const userId = req.params.id;
      // No type checking on userId
    });
    
    // After: routes/users.ts
    interface UserRequest extends Request {
      params: {
        id: string;
      }
    }
    
    router.get('/users/:id', (req: UserRequest, res: Response) => {
      const userId = req.params.id;
      // TypeScript ensures id exists and is a string
    });
    
  2. Created shared interfaces for database models:

    // models/User.ts
    export interface User {
      id: string;
      email: string;
      name: string;
      role: 'admin' | 'user' | 'guest';
      createdAt: Date;
    }
    
  3. Used declaration merging for Express:

    // types/express.d.ts
    import { User } from '../models/User';
    
    declare global {
      namespace Express {
        interface Request {
          currentUser?: User;
        }
      }
    }
    
  4. Results:

    • Caught 23 potential bugs during migration
    • API documentation automatically generated from types
    • Improved confidence in refactoring

General Migration Strategies

  1. Incremental adoption: Start by renaming .js files to .ts and fix errors as they come up.

    # Script to identify good migration candidates (low dependency files)
    npx madge --circular --extensions js,jsx src/ | sort -n
    
  2. Use declaration files: For libraries without TypeScript support, use declaration files (.d.ts).

    // declarations.d.ts
    declare module 'untyped-library' {
      export function doSomething(value: string): Promise<number>;
      // Define other functions/types here
    }
    
  3. Start with any: Use the any type initially, then gradually add more specific types.

    // Stage 1: Get it compiling with any
    function processData(data: any): any {
      return data.map(item => item.value);
    }
    
    // Stage 2: Add more specific types
    interface DataItem {
      id: number;
      value: string;
    }
    
    function processData(data: DataItem[]): string[] {
      return data.map(item => item.value);
    }
    
  4. Add TypeScript to your build process: Configure your bundler (webpack, Rollup, etc.) to handle TypeScript.

  5. Set up a linter: Use ESLint with TypeScript plugins to catch issues early.

    // .eslintrc.js
    module.exports = {
      parser: '@typescript-eslint/parser',
      plugins: ['@typescript-eslint'],
      extends: [
        'eslint:recommended',
        'plugin:@typescript-eslint/recommended'
      ],
      rules: {
        '@typescript-eslint/no-explicit-any': 'warn'
      }
    };
    
  • REST API with Express and TypeScript
  • React application with full type safety
  • CLI tool using Node.js and TypeScript
  • Library with proper type definitions

πŸ“‹ Quick Reference: TypeScript Cheat Sheet

Basic Types:

let str: string = "hello";
let num: number = 42;
let bool: boolean = true;
let arr: number[] = [1, 2, 3];
let obj: { name: string } = { name: "John" };

Function Types:

function fn(param: string): number { return 1; }
const arrow = (param: string): number => 1;
type FnType = (param: string) => number;

Interface Template:

interface MyInterface {
  required: string;
  optional?: number;
  readonly fixed: boolean;
}

Utility Types:

Partial<T>    // Makes all properties optional
Pick<T, K>    // Select specific properties
Omit<T, K>    // Exclude specific properties
Required<T>   // Makes all properties required

Type Guards:

function isString(x: unknown): x is string {
  return typeof x === 'string';
}

Remember: TypeScript is JavaScript with types. Start simple and add complexity as you grow!

Conclusion

TypeScript transforms JavaScript development by adding a powerful type system that catches errors early, improves code quality, and enhances developer experience. The key is starting simple and gradually adopting more advanced features as your projects grow.

🎯 Key Takeaways:

  • Start with basic types and let TypeScript infer when possible
  • Use interfaces for object shapes and contracts
  • Leverage utility types for common transformations
  • Write type guards for runtime type checking
  • Enable strict mode gradually for maximum safety
  • Focus on readability over complex type gymnastics

πŸš€ Next Steps:

  • Set up a TypeScript project using the configuration shown
  • Convert a small JavaScript project to TypeScript
  • Build the todo app example to practice the concepts
  • Join the TypeScript community and keep learning

Remember: TypeScript is not about making JavaScript harderβ€”it's about making it more reliable, maintainable, and enjoyable to work with. Every bug caught at compile time is time saved debugging in production.

Now go forth and build type-safe applications! πŸŽ‰

Happy coding! TypeScript will become second nature before you know it, and you'll wonder how you ever lived without it.

Billie Heidelberg Jr.

About Billie Heidelberg Jr.

Full Stack Developer & Technical Leader with 7+ years of experience building scalable applications and leading development teams. Passionate about sharing knowledge and helping others grow.

Want to Connect?

I'm always interested in discussing development challenges, trading technology, or potential collaboration opportunities.

Read more articles like this

← Back to all articles