TypeScript Fundamentals

TypeScript Fundamentals

TypeScript is a powerful superset of JavaScript that adds static typing to catch errors early and improve code quality. This comprehensive guide covers everything you need to know to get started with TypeScript.

What is TypeScript?

TypeScript is JavaScript with superpowers. It adds type safety, better tooling, and modern JavaScript features to your development workflow.

Why TypeScript?

  • Type Safety: Catch errors before runtime
  • Better IDE Support: Autocomplete and error checking
  • Self-Documenting: Types serve as documentation
  • Refactoring Safety: Code changes won’t break types
  • Industry Standard: Used by 70%+ of JavaScript projects
  • Gradual Adoption: Mix JavaScript and TypeScript

Installation and Setup

Installing TypeScript

# Install globally
npm install -g typescript

# Install in project
npm install --save-dev typescript

# Verify installation
tsc --version

# Install type definitions for common libraries
npm install --save-dev @types/node @types/react @types/express

Basic TypeScript Project

# Initialize project
mkdir my-ts-project
cd my-ts-project
npm init -y

# Install TypeScript locally
npm install --save-dev typescript @types/node

# Create TypeScript config file
npx tsc --init

# Create source directory
mkdir src
touch src/index.ts

TypeScript Configuration

Create tsconfig.json:

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

Basic Types

Primitive Types

// String
let name: string = "John Doe";
let message: string = `Hello, ${name}!`;

// Number
let age: number = 30;
let price: number = 19.99;
let count: number = 0;

// Boolean
let isActive: boolean = true;
let isLoggedIn: boolean = false;

// Null and Undefined
let nothing: null = null;
let notAssigned: undefined = undefined;

// Any (avoid when possible)
let anything: any = "can be anything";
anything = 42;
anything = true;

// Unknown (better than any)
let userInput: unknown = "user input";
if (typeof userInput === "string") {
    console.log((userInput as string).toUpperCase());
}

Arrays and Tuples

// Array with type annotation
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];

// Generic array
let items: Array<string> = ["item1", "item2"];
let values: ReadonlyArray<number> = [10, 20, 30];

// Tuple (fixed length array)
let person: [string, number] = ["John", 30];
let coordinates: [number, number] = [10, 20];

// Tuple with different types
let mixed: [string, number, boolean] = ["active", 25, true];

// Array destructuring with types
const [first, second, ...rest] = numbers;
console.log(first, second, rest); // 1 2 [3, 4, 5]

Objects and Interfaces

// Object type annotation
let user: {
    name: string;
    age: number;
    email: string;
} = {
    name: "Alice",
    age: 30,
    email: "[email protected]"
};

// Interface for object structure
interface Person {
    name: string;
    age: number;
    email?: string; // Optional property
    readonly id: number; // Read-only property
}

let employee: Person = {
    name: "Bob",
    age: 25,
    id: 123
};

// employee.id = 456; // Error: Cannot assign to readonly property

// Interface with methods
interface Calculator {
    add(a: number, b: number): number;
    subtract(a: number, b: number): number;
}

const simpleCalc: Calculator = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b
};

console.log(simpleCalc.add(5, 3)); // 8

Functions

Function Typing

// Basic function with parameter and return types
function add(a: number, b: number): number {
    return a + b;
}

// Arrow function with types
const multiply = (x: number, y: number): number => x * y;

// Function type annotation
let compute: (x: number, y: number) => number;

compute = (a, b) => a + b;

// Function with optional parameters
function greet(name: string = "Guest"): string {
    return `Hello, ${name}!`;
}

greet(); // Hello, Guest!
greet("Alice"); // Hello, Alice!

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

console.log(sum(1, 2, 3, 4, 5)); // 15

Function Overloading

// Function with multiple signatures
function process(input: string): string;
function process(input: number): number;
function process(input: boolean): boolean;

// Implementation
function process(input: any): any {
    if (typeof input === "string") {
        return input.toUpperCase();
    } else if (typeof input === "number") {
        return input * 2;
    } else if (typeof input === "boolean") {
        return !input;
    }
    return input;
}

console.log(process("hello")); // HELLO
console.log(process(5)); // 10
console.log(process(true)); // false

Advanced Types

Union Types

// Union types (multiple possible types)
let value: string | number;
value = "hello";
value = 42; // Both valid

// Union with literal types
type Theme = "light" | "dark";
type Status = "pending" | "success" | "error";

let currentTheme: Theme = "dark";
let apiStatus: Status = "success";

// Function with union parameter
function processValue(value: string | number): string {
    if (typeof value === "string") {
        return value.toUpperCase();
    } else {
        return value.toString();
    }
}

Intersection Types

// Combining multiple types
interface Person {
    name: string;
    age: number;
}

interface Employee {
    id: number;
    department: string;
}

type EmployeePerson = Person & Employee;

const worker: EmployeePerson = {
    name: "John",
    age: 30,
    id: 123,
    department: "Engineering"
};

Type Aliases

// Creating custom type names
type ID = string | number;
type Coordinates = {
    x: number;
    y: number;
};

// Using aliases
let userId: ID = "user123";
let position: Coordinates = { x: 10, y: 20 };

// Complex alias with generics
type ApiResponse<T> = {
    data: T;
    status: number;
    message: string;
};

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

const response: ApiResponse<User[]> = {
    data: [{ id: 1, name: "Alice" }],
    status: 200,
    message: "Success"
};

Classes

Basic Classes

class Animal {
    // Property with type
    private name: string;
    protected age: number;
    public species: string;

    // Constructor with parameter types
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
        this.species = "animal";
    }

    // Method with return type
    public makeSound(): string {
        return `${this.name} makes a sound`;
    }

    // Getter and setter with types
    get info(): string {
        return `${this.name} is ${this.age} years old`;
    }

    set newAge(age: number) {
        if (age > 0 && age < 30) {
            this.age = age;
        }
    }
}

const myAnimal = new Animal("Buddy", 5);
console.log(myAnimal.makeSound()); // Buddy makes a sound
console.log(myAnimal.info); // Buddy is 5 years old

Inheritance and Implements

// Interface for implementation
interface Drawable {
    draw(): void;
    area(): number;
}

// Base class
interface Shape {
    color: string;
}

class Rectangle implements Drawable, Shape {
    constructor(
        public width: number,
        public height: number,
        public color: string
    ) {}

    draw(): void {
        console.log(`Drawing ${this.color} rectangle`);
    }

    area(): number {
        return this.width * this.height;
    }
}

// Inherited class
class Square extends Rectangle {
    constructor(side: number, color: string) {
        super(side, side, color);
    }

    area(): number {
        return this.width * this.width; // Width = height for square
    }
}

const square = new Square(10, "blue");
square.draw(); // Drawing blue rectangle
console.log(square.area()); // 100

Abstract Classes

// Abstract class
abstract class Shape {
    constructor(protected color: string) {}

    abstract area(): number;
    abstract perimeter(): number;

    public getColor(): string {
        return this.color;
    }
}

// Concrete implementation
class Circle extends Shape {
    constructor(
        color: string,
        private radius: number
    ) {
        super(color);
    }

    area(): number {
        return Math.PI * this.radius * this.radius;
    }

    perimeter(): number {
        return 2 * Math.PI * this.radius;
    }
}

const circle = new Circle("red", 5);
console.log(circle.area()); // 78.54

Generics

Generic Functions

// Generic function
function identity<T>(arg: T): T {
    return arg;
}

const numResult: number = identity(42);
const strResult: string = identity("hello");

// Generic constraints
interface Lengthwise {
    length: number;
}

function getLength<T extends Lengthwise>(arg: T): number {
    return arg.length;
}

const arrayLength = getLength([1, 2, 3]); // 3
const stringLength = getLength("hello"); // 5

Generic Classes

class Box<T> {
    private content: T;

    constructor(value: T) {
        this.content = value;
    }

    getValue(): T {
        return this.content;
    }

    setValue(value: T): void {
        this.content = value;
    }
}

// Using with different types
const stringBox = new Box("hello");
const numberBox = new Box(42);

console.log(stringBox.getValue()); // hello
console.log(numberBox.getValue()); // 42

Multiple Generic Parameters

class KeyValueStore<K, V> {
    private items: Map<K, V> = new Map();

    set(key: K, value: V): void {
        this.items.set(key, value);
    }

    get(key: K): V | undefined {
        return this.items.get(key);
    }

    has(key: K): boolean {
        return this.items.has(key);
    }
}

// Usage
const store = new KeyValueStore<string, number>();
store.set("age", 30);
store.set("score", 100);

console.log(store.get("age")); // 30
console.log(store.has("score")); // true

Modules and Imports

Exporting Types and Classes

// types.ts
export interface User {
    id: number;
    name: string;
    email: string;
}

export type Theme = "light" | "dark";

// api.ts
export class ApiClient {
    constructor(private baseUrl: string) {}

    async getUser(id: number): Promise<User> {
        const response = await fetch(`${this.baseUrl}/users/${id}`);
        return response.json();
    }
}

// Default export
export default class Config {
    public apiUrl: string;
    public version: string = "1.0.0";
}

Importing Modules

// Named imports
import { User, Theme } from './types';
import { ApiClient } from './api';

// Default import
import Config from './config';

// Import everything
import * as Utils from './utils';

// Using imports
const user: User = { id: 1, name: "Alice", email: "[email protected]" };
const theme: Theme = "dark";

const api = new ApiClient("https://api.example.com");
const config = new Config();

Utility Types

Type Guards

// Type guard function
function isString(value: any): value is string {
    return typeof value === "string";
}

function isNumber(value: any): value is number {
    return typeof value === "number";
}

// Using type guards
function processValue(value: any): string {
    if (isString(value)) {
        return value.toUpperCase();
    } else if (isNumber(value)) {
        return value.toString();
    }
    return String(value);
}

console.log(processValue("hello")); // HELLO
console.log(processValue(42)); // 42

Type Assertion

// Type assertion
let unknownValue: unknown = "hello world";

// Using 'as' keyword
const length: number = (unknownValue as string).length;

// Using angle bracket syntax
const upperCase: string = (<string>unknownValue).toUpperCase();

// Unsafe assertion (be careful)
const anyValue: any = unknownValue as any;

// Safe assertion with type guard
function assertString(value: any): asserts value is string {
    if (typeof value !== "string") {
        throw new Error("Expected string");
    }
}

// Safe usage
function processString(value: unknown) {
    assertString(value);
    return value.toUpperCase(); // TypeScript knows this is string
}

Error Handling

Try-Catch with Types

// Error types in catch blocks
try {
    JSON.parse("invalid json");
} catch (error) {
    if (error instanceof SyntaxError) {
        console.log("JSON syntax error:", error.message);
    } else if (error instanceof Error) {
        console.log("General error:", error.message);
    }
}

// Custom error class
class ValidationError extends Error {
    constructor(public field: string, message: string) {
        super(`${field}: ${message}`);
    }
}

function validateEmail(email: string): void {
    if (!email.includes("@")) {
        throw new ValidationError("email", "Must contain @ symbol");
    }
}

try {
    validateEmail("invalid-email");
} catch (error) {
    if (error instanceof ValidationError) {
        console.log("Validation failed:", error.message);
    }
}

Working with DOM

Type-Safe DOM Manipulation

// Type-safe element selection
const button = document.getElementById("myButton") as HTMLButtonElement;
const input = document.querySelector(".myInput") as HTMLInputElement;
const elements = document.querySelectorAll(".item") as NodeListOf<HTMLElement>;

// Event handling with types
button.addEventListener("click", (event: MouseEvent) => {
    console.log("Button clicked at:", event.clientX, event.clientY);
});

input.addEventListener("input", (event: Event) => {
    const target = event.target as HTMLInputElement;
    console.log("Input value:", target.value);
});

// Creating elements with types
const div = document.createElement("div") as HTMLDivElement;
div.textContent = "Hello TypeScript";
div.style.color = "blue";

document.body.appendChild(div);

Type-Safe Forms

interface FormField {
    name: string;
    value: string;
    error?: string;
}

class Form {
    private fields: FormField[] = [];

    addField(field: FormField): void {
        this.fields.push(field);
    }

    getField(name: string): FormField | undefined {
        return this.fields.find(field => field.name === name);
    }

    setValue(name: string, value: string): void {
        const field = this.getField(name);
        if (field) {
            field.value = value;
            field.error = undefined;
        }
    }

    validate(): boolean {
        return this.fields.every(field => {
            if (field.name === "email" && !field.value.includes("@")) {
                field.error = "Invalid email";
                return false;
            }
            if (field.name === "password" && field.value.length < 6) {
                field.error = "Password too short";
                return false;
            }
            field.error = undefined;
            return true;
        });
    }
}

// Usage
const form = new Form();
form.addField({ name: "email", value: "" });
form.addField({ name: "password", value: "" });

const emailInput = document.querySelector("#email") as HTMLInputElement;
const passwordInput = document.querySelector("#password") as HTMLInputElement;

emailInput.addEventListener("input", (e) => {
    form.setValue("email", (e.target as HTMLInputElement).value);
});

passwordInput.addEventListener("input", (e) => {
    form.setValue("password", (e.target as HTMLInputElement).value);
});

function submitForm(): void {
    if (form.validate()) {
        console.log("Form is valid");
    } else {
        console.log("Form has errors");
    }
}

TypeScript with React

React Component Props

// Type-safe props interface
interface ButtonProps {
    text: string;
    onClick?: () => void;
    disabled?: boolean;
    variant?: "primary" | "secondary" | "danger";
}

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

// Usage
const App: React.FC = () => {
    const handleClick = () => {
        console.log("Button clicked!");
    };

    return (
        <div>
            <Button text="Click me" onClick={handleClick} />
            <Button text="Disabled" disabled={true} variant="secondary" />
        </div>
    );
};

useState with TypeScript

// User-defined hook type
interface User {
    id: number;
    name: string;
    email: string;
}

const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
    // useState with proper typing
    const [user, setUser] = useState<User | null>(null);
    const [loading, setLoading] = useState<boolean>(true);
    const [error, setError] = useState<string | null>(null);

    // Async function with proper typing
    const fetchUser = async (): Promise<void> => {
        try {
            setLoading(true);
            setError(null);
            
            const response = await fetch(`/api/users/${userId}`);
            const userData: User = await response.json();
            
            setUser(userData);
        } catch (err) {
            setError(err instanceof Error ? err.message : "Unknown error");
        } finally {
            setLoading(false);
        }
    };

    useEffect(() => {
        fetchUser();
    }, [userId]);

    if (loading) return <div>Loading user...</div>;
    if (error) return <div>Error: {error}</div>;
    if (!user) return <div>User not found</div>;

    return (
        <div>
            <h1>{user.name}</h1>
            <p>{user.email}</p>
        </div>
    );
};

Complete Example: Type-Safe Todo App

Project Structure

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

export interface TodoService {
    getTodos(): Promise<Todo[]>;
    addTodo(text: string): Promise<Todo>;
    toggleTodo(id: number): Promise<Todo>;
    deleteTodo(id: number): Promise<void>;
}

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

    async getTodos(): Promise<Todo[]> {
        return [...this.todos];
    }

    async addTodo(text: string): Promise<Todo> {
        const todo: Todo = {
            id: Date.now(),
            text,
            completed: false,
            createdAt: new Date()
        };
        this.todos.push(todo);
        return todo;
    }

    async toggleTodo(id: number): Promise<Todo> {
        const todo = this.todos.find(t => t.id === id);
        if (todo) {
            todo.completed = !todo.completed;
        }
        return todo as Todo;
    }

    async deleteTodo(id: number): Promise<void> {
        this.todos = this.todos.filter(t => t.id !== id);
    }
}

// TodoApp.tsx
import React, { useState, useEffect } from 'react';
import { Todo, TodoService } from './types';

const TodoApp: React.FC<{ service: TodoService }> = ({ service }) => {
    const [todos, setTodos] = useState<Todo[]>([]);
    const [input, setInput] = useState<string>("");
    const [loading, setLoading] = useState<boolean>(true);

    useEffect(() => {
        service.getTodos().then(setTodos).finally(() => setLoading(false));
    }, [service]);

    const addTodo = async (): Promise<void> => {
        if (input.trim()) {
            setLoading(true);
            const newTodo = await service.addTodo(input);
            setTodos(prev => [...prev, newTodo]);
            setInput("");
            setLoading(false);
        }
    };

    const toggleTodo = async (id: number): Promise<void> => {
        setLoading(true);
        const updatedTodo = await service.toggleTodo(id);
        setTodos(prev => prev.map(todo => 
            todo.id === id ? updatedTodo : todo
        ));
        setLoading(false);
    };

    return (
        <div className="todo-app">
            <h1>Todo List</h1>
            
            <div className="todo-input">
                <input
                    type="text"
                    value={input}
                    onChange={(e) => setInput(e.currentTarget.value)}
                    placeholder="What needs to be done?"
                    onKeyPress={(e) => {
                        if (e.key === "Enter") {
                            addTodo();
                        }
                    }}
                />
                <button onClick={addTodo} disabled={!input.trim()}>
                    Add
                </button>
            </div>

            {loading ? (
                <div>Loading...</div>
            ) : (
                <ul className="todo-list">
                    {todos.map(todo => (
                        <li key={todo.id} className={todo.completed ? "completed" : ""}>
                            <span>{todo.text}</span>
                            <button onClick={() => toggleTodo(todo.id)}>
                                {todo.completed ? "Undo" : "Complete"}
                            </button>
                        </li>
                    ))}
                </ul>
            )}
        </div>
    );
};

// App.tsx
import React from 'react';
import { TodoApp } from './TodoApp';
import { LocalTodoService } from './todoService';

const App: React.FC = () => {
    const todoService = new LocalTodoService();

    return (
        <div>
            <h1>TypeScript Todo App</h1>
            <TodoApp service={todoService} />
        </div>
    );
};

Best Practices

TypeScript Configuration

{
  "compilerOptions": {
    // Strict mode for better type checking
    "strict": true,
    
    // No implicit any
    "noImplicitAny": true,
    
    // Strict null checks
    "strictNullChecks": true,
    
    // Prevent unused variables
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    
    // Consistent casing
    "forceConsistentCasingInFileNames": true,
    
    // Module resolution
    "moduleResolution": "node",
    "baseUrl": "./src",
    "paths": {
        "@/*": ["*"],
        "@/components/*": ["components/*"],
        "@/utils/*": ["utils/*"]
    }
  }
}

Code Organization

// types/index.ts - Export all types
export interface User { ... }
export interface Todo { ... }
export type Theme = "light" | "dark";

// utils/index.ts - Utility functions with types
export const formatDate = (date: Date): string => { ... };
export const validateEmail = (email: string): boolean => { ... };

// components/index.ts - Component exports
export { Button } from './Button';
export { Input } from './Input';
export { Card } from './Card';

External Resources:

Related Tutorials:

Last updated on