JavaScript11/19/2025⏱️ 20 min read
TypeScript Complete Guide: From JavaScript to Type-Safe Code
TypeScriptJavaScriptType SafetyProgrammingWeb DevelopmentFrontend

TypeScript Complete Guide: From JavaScript to Type-Safe Code

Introduction

TypeScript has become one of the most popular programming languages in web development, offering type safety, better tooling, and improved developer experience over JavaScript. Created by Microsoft, TypeScript is a superset of JavaScript that compiles to plain JavaScript, making it compatible with any JavaScript runtime.

This comprehensive guide will take you from TypeScript basics to advanced features, showing you how to write type-safe, maintainable code. Whether you're migrating from JavaScript or starting fresh, you'll learn how to leverage TypeScript's powerful type system to build robust applications.

What is TypeScript?

TypeScript is a statically typed programming language that builds on JavaScript by adding type definitions. It provides:

Key Features:

  • Static Type Checking: Catch errors at compile time
  • Type Inference: Automatic type detection
  • Modern JavaScript Features: ES6+ support with transpilation
  • Better IDE Support: Enhanced autocomplete and refactoring
  • Gradual Adoption: Can be adopted incrementally
  • Interoperability: Works with existing JavaScript code

Benefits:

  • Early Error Detection: Find bugs before runtime
  • Better Documentation: Types serve as inline documentation
  • Improved Refactoring: Safe code changes with confidence
  • Enhanced Developer Experience: Better autocomplete and IntelliSense
  • Scalability: Easier to maintain large codebases

TypeScript vs JavaScript:

  • TypeScript adds types but compiles to JavaScript
  • All JavaScript is valid TypeScript
  • TypeScript provides compile-time type checking
  • JavaScript is dynamically typed, TypeScript is statically typed

Getting Started with TypeScript

Setting up TypeScript is straightforward:

# Install TypeScript globally
npm install -g typescript

# Or install locally in your project
npm install --save-dev typescript

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

# Create tsconfig.json
npx tsc --init

# Compile TypeScript files
tsc

# Watch mode for development
tsc --watch

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

// src/index.ts
function greet(name: string): string {
    return `Hello, ${name}!`;
}

const message: string = greet('TypeScript');
console.log(message);

tsc
node dist/index.js

TypeScript Basics: Types and Annotations

TypeScript's type system is its core feature:

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

// Arrays
let numbers: number[] = [1, 2, 3, 4, 5];
let names: Array<string> = ['John', 'Jane', 'Bob'];

// Tuples
let person: [string, number] = ['John', 30];

// Enums
enum Color {
    Red = 'RED',
    Green = 'GREEN',
    Blue = 'BLUE'
}

let favoriteColor: Color = Color.Red;

// Any (avoid when possible)
let anything: any = 'can be anything';

// Unknown (safer than any)
let userInput: unknown;
if (typeof userInput === 'string') {
    let str: string = userInput;
}

// Void
function logMessage(message: string): void {
    console.log(message);
}

// Never
function throwError(message: string): never {
    throw new Error(message);
}

// TypeScript infers types automatically
let name = 'John'; // inferred as string
let age = 30; // inferred as number
let isActive = true; // inferred as boolean

// Explicit type annotations
let userName: string = 'John';
let userAge: number = 30;

// Union types allow multiple types
let id: string | number;
id = '123';
id = 123;

// Type guards
function processId(id: string | number) {
    if (typeof id === 'string') {
        console.log(id.toUpperCase());
    } else {
        console.log(id.toFixed(2));
    }
}

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

interface Employee {
    employeeId: string;
    department: string;
}

type EmployeePerson = Person & Employee;

const employee: EmployeePerson = {
    name: 'John',
    age: 30,
    employeeId: 'E123',
    department: 'Engineering'
};

Interfaces and Type Aliases

Interfaces and type aliases define object shapes:

// Basic interface
interface User {
    id: number;
    name: string;
    email: string;
    age?: number; // Optional property
    readonly createdAt: Date; // Read-only property
}

// Using interface
const user: User = {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com',
    createdAt: new Date()
};

// Extending interfaces
interface Admin extends User {
    role: 'admin' | 'super-admin';
    permissions: string[];
}

const admin: Admin = {
    id: 1,
    name: 'Admin User',
    email: 'admin@example.com',
    createdAt: new Date(),
    role: 'admin',
    permissions: ['read', 'write', 'delete']
};

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

// Implementing interface
class BasicCalculator implements Calculator {
    add(a: number, b: number): number {
        return a + b;
    }
    
    subtract(a: number, b: number): number {
        return a - b;
    }
    
    multiply(a: number, b: number): number {
        return a * b;
    }
    
    divide(a: number, b: number): number {
        if (b === 0) throw new Error('Division by zero');
        return a / b;
    }
}

// Type aliases for unions
type Status = 'pending' | 'approved' | 'rejected';

// Type aliases for objects
type Point = {
    x: number;
    y: number;
};

// Type aliases for functions
type MathOperation = (a: number, b: number) => number;

// Type aliases for complex types
type UserWithStatus = User & {
    status: Status;
};

// Differences between interface and type
// Interfaces can be extended and merged
// Types can represent unions, intersections, and primitives

Classes and Object-Oriented Programming

TypeScript enhances JavaScript classes with type safety:

class Person {
    // Public properties (default)
    name: string;
    age: number;
    
    // Private properties
    private email: string;
    
    // Protected properties
    protected id: number;
    
    // Readonly properties
    readonly createdAt: Date;
    
    // Constructor
    constructor(name: string, age: number, email: string) {
        this.name = name;
        this.age = age;
        this.email = email;
        this.id = Math.random();
        this.createdAt = new Date();
    }
    
    // Public method
    greet(): string {
        return `Hello, I'm ${this.name}`;
    }
    
    // Private method
    private validateEmail(): boolean {
        return this.email.includes('@');
    }
    
    // Getter
    get userInfo(): string {
        return `${this.name} (${this.email})`;
    }
    
    // Setter
    set userAge(newAge: number) {
        if (newAge < 0) {
            throw new Error('Age cannot be negative');
        }
        this.age = newAge;
    }
}

// Using the class
const person = new Person('John', 30, 'john@example.com');
console.log(person.greet());
console.log(person.userInfo);

// Base class
class Animal {
    protected name: string;
    
    constructor(name: string) {
        this.name = name;
    }
    
    move(distance: number = 0): void {
        console.log(`${this.name} moved ${distance}m`);
    }
}

// Derived class
class Dog extends Animal {
    private breed: string;
    
    constructor(name: string, breed: string) {
        super(name); // Call parent constructor
        this.breed = breed;
    }
    
    bark(): void {
        console.log(`${this.name} barks!`);
    }
    
    // Override method
    move(distance: number = 5): void {
        console.log(`${this.name} runs ${distance}m`);
        super.move(distance);
    }
}

const dog = new Dog('Buddy', 'Golden Retriever');
dog.bark();
dog.move(10);

// Abstract class
abstract class Shape {
    protected color: string;
    
    constructor(color: string) {
        this.color = color;
    }
    
    // Abstract method (must be implemented by derived classes)
    abstract calculateArea(): number;
    
    // Concrete method
    getColor(): string {
        return this.color;
    }
}

// Concrete implementation
class Circle extends Shape {
    private radius: number;
    
    constructor(color: string, radius: number) {
        super(color);
        this.radius = radius;
    }
    
    calculateArea(): number {
        return Math.PI * this.radius * this.radius;
    }
}

class Rectangle extends Shape {
    private width: number;
    private height: number;
    
    constructor(color: string, width: number, height: number) {
        super(color);
        this.width = width;
        this.height = height;
    }
    
    calculateArea(): number {
        return this.width * this.height;
    }
}

Generics

Generics provide type-safe, reusable code:

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

// Usage
let output1 = identity<string>('hello');
let output2 = identity<number>(42);
let output3 = identity('hello'); // Type inference

// Generic interface
interface Box<T> {
    value: T;
}

const stringBox: Box<string> = { value: 'hello' };
const numberBox: Box<number> = { value: 42 };

// Generic class
class Container<T> {
    private items: T[] = [];
    
    add(item: T): void {
        this.items.push(item);
    }
    
    get(index: number): T | undefined {
        return this.items[index];
    }
    
    getAll(): T[] {
        return [...this.items];
    }
}

const stringContainer = new Container<string>();
stringContainer.add('hello');
stringContainer.add('world');

const numberContainer = new Container<number>();
numberContainer.add(1);
numberContainer.add(2);

// Constraint with interface
interface Lengthwise {
    length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

logLength('hello'); // OK
logLength([1, 2, 3]); // OK
// logLength(42); // Error: number doesn't have length

// Multiple constraints
function combine<T extends object, U extends object>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

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

const person = { name: 'John', age: 30 };
getProperty(person, 'name'); // OK
getProperty(person, 'age'); // OK
// getProperty(person, 'email'); // Error: 'email' doesn't exist

// Partial - makes all properties optional
interface User {
    name: string;
    age: number;
    email: string;
}

type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string; }

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

// Readonly - makes all properties readonly
type ReadonlyUser = Readonly<User>;

// Pick - select specific properties
type UserName = Pick<User, 'name'>;

// Omit - exclude specific properties
type UserWithoutEmail = Omit<User, 'email'>;

// Record - create object type
type UserMap = Record<string, User>;

// Exclude - exclude types from union
type NonNullable<T> = Exclude<T, null | undefined>;

// Extract - extract types from union
type StringKeys = Extract<keyof User, string>;

Advanced TypeScript Features

TypeScript offers advanced features for complex scenarios:

// Enable decorators in tsconfig.json
// "experimentalDecorators": true

// Class decorator
function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed
class Greeter {
    greeting: string;
    
    constructor(message: string) {
        this.greeting = message;
    }
}

// Method decorator
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function (...args: any[]) {
        console.log(`Calling ${propertyKey} with args:`, args);
        const result = originalMethod.apply(this, args);
        console.log(`Result:`, result);
        return result;
    };
    
    return descriptor;
}

class Calculator {
    @log
    add(a: number, b: number): number {
        return a + b;
    }
}

// Conditional type
type NonNullable<T> = T extends null | undefined ? never : T;

// Infer keyword
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

type FunctionReturnType = ReturnType<() => string>; // string

// Template literal types
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'

// Mapped types
type Optional<T> = {
    [K in keyof T]?: T[K];
};

// Augment existing modules
declare module 'express' {
    interface Request {
        user?: User;
    }
}

// Now Request has user property
import { Request } from 'express';
const req: Request = {};
req.user; // OK

TypeScript with Popular Frameworks

TypeScript integrates seamlessly with popular frameworks:

// Functional components
import React from 'react';

interface Props {
    name: string;
    age?: number;
}

const Greeting: React.FC<Props> = ({ name, age }) => {
    return (
        <div>
            <h1>Hello, {name}!</h1>
            {age && <p>You are {age} years old</p>}
        </div>
    );
};

// Class components
interface State {
    count: number;
}

class Counter extends React.Component<{}, State> {
    state: State = {
        count: 0
    };
    
    increment = (): void => {
        this.setState({ count: this.state.count + 1 });
    };
    
    render() {
        return (
            <div>
                <p>Count: {this.state.count}</p>
                <button onClick={this.increment}>Increment</button>
            </div>
        );
    }
}

// Hooks with TypeScript
import { useState, useEffect } from 'react';

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

// Express with TypeScript
import express, { Request, Response, NextFunction } from 'express';

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

const app = express();

app.get('/users/:id', (req: Request, res: Response, next: NextFunction) => {
    const userId: number = parseInt(req.params.id);
    const user: User = {
        id: userId,
        name: 'John Doe',
        email: 'john@example.com'
    };
    res.json(user);
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
});

Migrating from JavaScript to TypeScript

Migrating to TypeScript can be done gradually:

npm install --save-dev typescript @types/node
npx tsc --init

# Rename .js files to .ts
mv src/index.js src/index.ts

// Before (JavaScript)
function add(a, b) {
    return a + b;
}

// After (TypeScript)
function add(a: number, b: number): number {
    return a + b;
}

{
  "compilerOptions": {
    "allowJs": true, // Allow JavaScript files
    "checkJs": false, // Don't check JavaScript files initially
    "noImplicitAny": false, // Allow implicit any initially
    "strict": false // Start with loose settings
  }
}

{
  "compilerOptions": {
    "strict": true, // Enable all strict checks
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true
  }
}

Best Practices:

  • Start with `allowJs: true` to mix JS and TS
  • Add types incrementally
  • Use `any` sparingly, prefer `unknown`
  • Enable strict mode gradually
  • Use type inference when possible
  • Leverage existing JavaScript libraries with `@types` packages

TypeScript Best Practices

Follow these best practices for better TypeScript code:

// Good - let TypeScript infer types
const name = 'John';
const age = 30;

// Avoid unnecessary type annotations
const name: string = 'John'; // Redundant

// Good - use interfaces for object shapes
interface User {
    name: string;
    age: number;
}

// Use types for unions, intersections, primitives
type Status = 'active' | 'inactive';

{
  "compilerOptions": {
    "strict": true
  }
}

// Bad
function processData(data: any) {
    // No type safety
}

// Good
function processData(data: unknown) {
    if (typeof data === 'string') {
        // Type-safe handling
    }
}

// Use built-in utility types
type PartialUser = Partial<User>;
type RequiredUser = Required<User>;
type ReadonlyUser = Readonly<User>;

/**
 * Represents a user in the system
 * @property id - Unique identifier
 * @property name - User's full name
 * @property email - User's email address
 */
interface User {
    id: number;
    name: string;
    email: string;
}

Conclusion

TypeScript brings type safety, better tooling, and improved developer experience to JavaScript development. By understanding TypeScript's type system, generics, and advanced features, you can write more maintainable, scalable code.

Start with the basics—types, interfaces, and classes—then gradually explore advanced features like generics, decorators, and conditional types. Remember that TypeScript is designed to be adopted incrementally, so you can migrate your JavaScript codebase gradually.

With TypeScript, you can catch errors early, improve code documentation, and build more robust applications. Whether you're building frontend applications with React or backend services with Node.js, TypeScript provides the tools you need to write better code.

Embrace TypeScript's type system, leverage its powerful features, and enjoy the benefits of type-safe JavaScript development.

Share this article

Comments