Isaac.

TypeScript Basics

Learn the fundamentals of TypeScript and how it enhances JavaScript with type safety.

By EMEPublished: February 20, 2025
typescriptjavascripttypestype safetyweb developmentnode.js

A Simple Analogy

Imagine a letter carrier delivering mail. In JavaScript (regular mail), anyone can write anything on the envelope, and sometimes it gets lost or sent to the wrong place. In TypeScript (certified mail), you specify exactly who the letter is for, what's inside, and how heavy it is. The postal service (compiler) checks everything before delivery and warns you if something looks wrong.


What Is TypeScript?

TypeScript is a programming language built on top of JavaScript that adds static typing. It allows you to declare what type (string, number, boolean, custom objects) a variable should be, and the compiler checks for errors before you run your code.


Why Use TypeScript?

  • Catch errors early: Type mismatches are caught during development, not in production
  • Better IDE support: Your editor can autocomplete and warn about mistakes
  • Self-documenting code: Types show what a function expects and returns
  • Refactoring confidence: Change code safely knowing the compiler will catch breaking changes
  • Scalability: Large projects stay maintainable with clear type contracts
  • Better collaboration: Teams understand what data each function needs

Basic Types

Primitive Types

// String
const name: string = "Alice";

// Number
const age: number = 30;
const price: number = 19.99;

// Boolean
const isActive: boolean = true;

// Undefined and Null
const nothing: undefined = undefined;
const empty: null = null;

// Any (avoid when possible - defeats the purpose!)
const mystery: any = "could be anything";

Arrays

// Array of strings
const names: string[] = ["Alice", "Bob"];

// Array of numbers
const scores: number[] = [95, 87, 92];

// Alternative syntax
const colors: Array<string> = ["red", "blue"];

// Mixed (if needed)
const mixed: (string | number)[] = ["hello", 42];

Union Types (Multiple Options)

// A variable can be one of several types
const id: string | number = 123;  // Can be string or number
const result: string | null = getValue();  // Might be null

Literal Types (Specific Values)

// Only these exact values allowed
type Status = "pending" | "active" | "inactive";
const userStatus: Status = "active";  // ✓ OK
// userStatus = "unknown";  // ✗ Error

Interfaces & Types

Interfaces (Describing Objects)

// Define what a User object should look like
interface User {
    id: number;
    name: string;
    email: string;
    isActive?: boolean;  // Optional (with ?)
}

// Now you can use it
const user: User = {
    id: 1,
    name: "Alice",
    email: "alice@example.com",
    isActive: true,
};

Type Aliases

// Similar to interface, but more flexible
type Address = {
    street: string;
    city: string;
    zip: string;
};

const address: Address = {
    street: "123 Main St",
    city: "Portland",
    zip: "97201",
};

Differences (Simplified)

// Interface: better for object shapes, extendable
interface Animal {
    name: string;
}

interface Dog extends Animal {
    breed: string;
}

// Type: more flexible, supports unions
type Size = "small" | "medium" | "large";

Functions with Types

Basic Function Types

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

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

// Function with optional parameter
function describe(name: string, age?: number): string {
    return age ? `${name} is ${age}` : `${name}'s age unknown`;
}

// Function with default value
function setDelay(ms: number = 1000): void {
    setTimeout(() => {}, ms);
}

Function with Complex Types

interface OrderItem {
    productId: number;
    quantity: number;
    price: number;
}

function calculateOrderTotal(items: OrderItem[]): number {
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

// Usage
const items: OrderItem[] = [
    { productId: 1, quantity: 2, price: 10 },
    { productId: 2, quantity: 1, price: 20 },
];

const total = calculateOrderTotal(items);  // ✓ Type-safe

Classes with Types

class User {
    id: number;
    name: string;
    email: string;

    constructor(id: number, name: string, email: string) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    getDisplayName(): string {
        return `${this.name} (${this.email})`;
    }
}

const user = new User(1, "Alice", "alice@example.com");
console.log(user.getDisplayName());  // "Alice (alice@example.com)"

Access Modifiers

class Account {
    public balance: number;      // Anyone can access
    private pin: number;         // Only class can access
    protected accountType: string; // Class and subclasses can access

    constructor(balance: number, pin: number) {
        this.balance = balance;
        this.pin = pin;
    }

    withdraw(amount: number): void {
        if (amount <= this.balance) {
            this.balance -= amount;
        }
    }
}

Generics (Reusable Types)

// Generic function - works with any type
function getFirstElement<T>(array: T[]): T | undefined {
    return array[0];
}

const firstString = getFirstElement(["a", "b"]);  // Type: string
const firstNumber = getFirstElement([1, 2, 3]);   // Type: number

// Generic interface
interface Container<T> {
    value: T;
    getValue(): T;
}

const stringContainer: Container<string> = {
    value: "hello",
    getValue() { return this.value; }
};

const numberContainer: Container<number> = {
    value: 42,
    getValue() { return this.value; }
};

Practical Examples

E-commerce Product System

// Define types
interface Product {
    id: number;
    name: string;
    price: number;
    inStock: boolean;
}

interface CartItem {
    product: Product;
    quantity: number;
}

interface Cart {
    items: CartItem[];
    getTotal(): number;
}

// Implement with type safety
class ShoppingCart implements Cart {
    items: CartItem[] = [];

    addItem(product: Product, quantity: number): void {
        const existing = this.items.find(item => item.product.id === product.id);
        if (existing) {
            existing.quantity += quantity;
        } else {
            this.items.push({ product, quantity });
        }
    }

    getTotal(): number {
        return this.items.reduce(
            (sum, item) => sum + item.product.price * item.quantity,
            0
        );
    }
}

// Usage with full type safety
const cart = new ShoppingCart();
const laptop: Product = { id: 1, name: "Laptop", price: 999, inStock: true };
cart.addItem(laptop, 1);
console.log(cart.getTotal());  // 999

API Response Handling

interface ApiResponse<T> {
    success: boolean;
    data?: T;
    error?: string;
}

async function fetchUser(id: number): Promise<ApiResponse<User>> {
    try {
        const response = await fetch(`/api/users/${id}`);
        const data = await response.json();
        return { success: true, data };
    } catch (error) {
        return { success: false, error: error.message };
    }
}

// Type-safe usage
const response = await fetchUser(1);
if (response.success && response.data) {
    console.log(response.data.name);  // ✓ TypeScript knows data is a User
}

Real-World Use Cases

  • Web applications: React, Vue, Angular with full type safety
  • Node.js backends: Express, NestJS with typed APIs
  • Full-stack development: Share types between frontend and backend
  • Large teams: Types serve as contracts, preventing miscommunication
  • Refactoring: Compiler catches breaking changes across the codebase

Best Practices

  • Use string, number, boolean for primitive types
  • Define interfaces for object shapes
  • Use unions for values that can be multiple types
  • Add readonly for immutable properties
  • Use generics for reusable, type-safe components
  • Avoid any (use unknown if you must)
  • Enable strict mode in tsconfig.json

Common Pitfalls

Using any (Defeats the Purpose)

// Bad
const user: any = someData;

// Good
const user: User = someData;

Over-Complicating Types

// Too complex
type ComplexType = string | number | boolean | object | Array<any>;

// Better
type Value = string | number;

Forgetting Type Imports

// This might cause issues at runtime
import User from "./User";  // Is this a type or class?

// Better - explicit
import type { User } from "./User";  // Type only
import { UserClass } from "./User";   // Runtime value

TypeScript Configuration

Key tsconfig.json Settings

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  }
}

Related Concepts to Explore

  • Type guards and type narrowing
  • Decorators and metadata
  • Modules and namespaces
  • Advanced generics and constraints
  • Conditional types
  • Mapped types
  • Type inference
  • JSDoc type annotations
  • Flow (alternative to TypeScript)
  • Type testing with libraries like tsd

Summary

TypeScript transforms JavaScript from a loosely-typed language into one with strong type safety. By declaring what types your variables, functions, and objects should be, you catch mistakes early, get better IDE support, and write code that's self-documenting. Start with basic types, graduate to interfaces and generics, and you'll build more reliable, maintainable applications that scale from hobby projects to enterprise systems.