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
- Why TypeScript Matters
- Setting Up Your Environment
- Type System Fundamentals
- Practical Typing Patterns
- Working with Objects & Functions
- Advanced Types Made Simple
- Real-World Integration
- Common Pitfalls & Solutions
- 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:
anyandunknownare at the top of the hierarchy (least type-safe)- Primitive types form the foundation of the system
nullandundefinedare 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.jsonsettings - 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:
-
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) -
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"] } -
Prioritized high-value, low-risk components first:
- Utility functions (date formatting, string manipulation)
- API service layers
- State management stores
-
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:
-
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 }); -
Created shared interfaces for database models:
// models/User.ts export interface User { id: string; email: string; name: string; role: 'admin' | 'user' | 'guest'; createdAt: Date; } -
Used declaration merging for Express:
// types/express.d.ts import { User } from '../models/User'; declare global { namespace Express { interface Request { currentUser?: User; } } } -
Results:
- Caught 23 potential bugs during migration
- API documentation automatically generated from types
- Improved confidence in refactoring
General Migration Strategies
-
Incremental adoption: Start by renaming
.jsfiles to.tsand fix errors as they come up.# Script to identify good migration candidates (low dependency files) npx madge --circular --extensions js,jsx src/ | sort -n -
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 } -
Start with
any: Use theanytype 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); } -
Add TypeScript to your build process: Configure your bundler (webpack, Rollup, etc.) to handle TypeScript.
-
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' } };
Recommended Projects:
- 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.




