javascriptroom guide

The Ultimate TypeScript Handbook for JavaScript Developers

TypeScript (TS) is a *superset* of JavaScript (JS) developed by Microsoft. It adds optional static typing, enabling developers to define types for variables, functions, and objects. When compiled, TypeScript transpiles to plain JavaScript, ensuring compatibility with all JS environments (browsers, Node.js, etc.).

TypeScript has rapidly become an essential tool for JavaScript developers, offering the flexibility of JavaScript with the safety and tooling of static types. Whether you’re building a small script or a large-scale application, TypeScript helps catch errors early, improves code readability, and enhances collaboration. This handbook is designed to take you from JavaScript familiarity to TypeScript proficiency, with practical examples and clear explanations.

Table of Contents

  1. Introduction to TypeScript
  2. Setting Up TypeScript
  3. Core TypeScript Concepts
  4. Enums
  5. Modules and Namespaces
  6. Decorators
  7. Migrating from JavaScript to TypeScript
  8. Tooling and Debugging
  9. Conclusion
  10. References

Why TypeScript?

  • Early Error Detection: Catches type-related bugs during development (e.g., passing a string to a function expecting a number).
  • Improved Tooling: IDEs like VS Code provide autocompletion, refactoring, and inline documentation using TypeScript’s type information.
  • Readability and Maintainability: Types act as self-documenting code, making it easier to understand how functions and components work.
  • Scalability: Ideal for large codebases, where type safety reduces regressions and simplifies collaboration.

Setting Up TypeScript

Installation

First, install TypeScript globally via npm or yarn:

npm install -g typescript  
# or  
yarn global add typescript  

Verify installation:

tsc --version  # Output: Version 5.x.x (or latest)  

Project Setup

  1. Create a new project folder and initialize package.json:

    mkdir ts-handbook && cd ts-handbook  
    npm init -y  
  2. Create a TypeScript configuration file (tsconfig.json). This file defines compiler options:

    tsc --init  
  3. Key tsconfig.json Options:

    OptionPurpose
    targetSpecifies the JS version to transpile to (e.g., ES6, ESNext).
    moduleDefines the module system (e.g., ES6, CommonJS).
    outDirOutput directory for compiled JS files (e.g., ./dist).
    rootDirRoot directory of TS source files (e.g., ./src).
    strictEnables strict type-checking (recommended: true for maximum safety).
    sourceMapGenerates source maps for debugging (maps TS code to JS).

    Example tsconfig.json (minimal):

    {  
      "compilerOptions": {  
        "target": "ES6",  
        "module": "ES6",  
        "outDir": "./dist",  
        "rootDir": "./src",  
        "strict": true,  
        "sourceMap": true  
      },  
      "include": ["src/**/*"],  # Include all TS files in src/  
      "exclude": ["node_modules"]  # Exclude dependencies  
    }  
  4. Write your first TypeScript file:
    Create src/index.ts:

    // src/index.ts  
    function greet(name: string): string {  
      return `Hello, ${name}!`;  
    }  
    
    const message = greet("TypeScript Developer");  
    console.log(message);  // Output: Hello, TypeScript Developer!  
  5. Compile and run:

    tsc  # Compiles TS to JS in ./dist  
    node dist/index.js  # Runs the compiled JS  

Core TypeScript Concepts

Primitive Types

TypeScript supports all JavaScript primitive types, with explicit type annotations:

TypeDescriptionExample
numberIntegers, floats, hex, binary, or octalconst age: number = 25;
stringText, with optional template literalsconst name: string = "Alice";
booleantrue or falseconst isActive: boolean = true;
nullExplicit absence of value (strict mode only)const empty: null = null;
undefinedUninitialized valueconst missing: undefined = undefined;
bigintLarge integers (ES2020+)const big: bigint = 100n;
symbolUnique, immutable valuesconst id: symbol = Symbol("id");

Note: In strict mode, null and undefined are not subtypes of other types. Use | to allow them (e.g., string | null).

Any, Unknown, and Never

These types handle dynamic or untyped values, but with different safety levels:

any

Disables type checking for a value (use sparingly!):

let randomValue: any = "hello";  
randomValue = 42;  // No error  
randomValue.toUpperCase();  // No error (even if it’s a number later)  

When to use: Interoping with untyped JS libraries temporarily.

unknown

Safer alternative to any: the value’s type is unknown, so you must validate it before use:

let userInput: unknown = "hello";  

// Error: Object is of type 'unknown'  
userInput.toUpperCase();  

// Fix: Narrow the type with a check  
if (typeof userInput === "string") {  
  console.log(userInput.toUpperCase());  // HELLO  
}  

never

Represents values that never occur. Use for functions that throw errors or infinite loops:

// Function that throws an error  
function throwError(message: string): never {  
  throw new Error(message);  
}  

// Infinite loop  
function infiniteLoop(): never {  
  while (true) {}  
}  

Interfaces vs. Type Aliases

Both define shapes of objects, but they have key differences:

Interfaces

Use interface to define object types. Interfaces can be extended and merged:

// Define an interface  
interface User {  
  id: number;  
  name: string;  
  email?: string;  // Optional property (denoted with ?)  
}  

// Extend an interface  
interface AdminUser extends User {  
  role: "admin";  // Literal type (only "admin" allowed)  
}  

const admin: AdminUser = {  
  id: 1,  
  name: "Bob",  
  role: "admin"  // Valid  
};  

Type Aliases

Use type to create a alias for any type (not just objects). Type aliases cannot be reopened or merged:

// Type alias for an object  
type Product = {  
  id: number;  
  name: string;  
  price: number;  
};  

// Type alias for a union  
type Status = "pending" | "approved" | "rejected";  

const orderStatus: Status = "pending";  // Valid  

When to use which?

  • Use interface for object shapes that may need extension (e.g., class hierarchies).
  • Use type for unions, tuples, or non-object types (e.g., type ID = string | number).

Generics

Generics enable reusable components that work with multiple types without losing type information. Think of them as “type variables.”

Basic Example: Identity Function

A function that returns its input, preserving the input type:

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

// Usage with explicit type  
const str: string = identity<string>("hello");  

// Type inferred automatically  
const num = identity(42);  // num is type number  

Generic Interfaces/Classes

Use generics to make interfaces or classes flexible:

// Generic interface for an API response  
interface ApiResponse<T> {  
  data: T;  
  status: number;  
  message?: string;  
}  

// Usage with a User type  
type User = { id: number; name: string };  
const userResponse: ApiResponse<User> = {  
  data: { id: 1, name: "Alice" },  
  status: 200  
};  

Utility Types

TypeScript provides built-in utility types to transform existing types. Here are the most useful ones:

UtilityPurposeExample
Partial<T>Makes all properties of T optionalPartial<User>{ id?: number; name?: string }
Required<T>Makes all properties of T requiredRequired<Partial<User>>User
Readonly<T>Makes all properties of T read-onlyReadonly<User>{ readonly id: number; ... }
Pick<T, K>Selects subset K of properties from T`Pick<User, “id"
Omit<T, K>Removes subset K of properties from TOmit<User, "email">{ id: number; name: string }

Example: Using Partial
Update a user object without redefining all properties:

type User = { id: number; name: string; email: string };  

function updateUser(user: User, changes: Partial<User>): User {  
  return { ...user, ...changes };  
}  

const user: User = { id: 1, name: "Alice", email: "[email protected]" };  
const updatedUser = updateUser(user, { name: "Alicia" });  // { id: 1, name: "Alicia", email: "[email protected]" }  

Enums

Enums (enumerations) define a set of named constants. They improve readability by replacing “magic numbers” with meaningful names.

Numeric Enums

Default to incremental numbers (starts at 0):

enum Status {  
  Pending,   // 0  
  Approved,  // 1  
  Rejected   // 2  
}  

const currentStatus = Status.Approved;  
console.log(currentStatus);  // 1  

String Enums

Explicitly set string values (safer than numeric enums):

enum Direction {  
  Up = "UP",  
  Down = "DOWN",  
  Left = "LEFT",  
  Right = "RIGHT"  
}  

const move = Direction.Right;  // "RIGHT"  

When to Use Enums?

Use enums for fixed sets of values (e.g., status codes, directions). For dynamic values, prefer union types (e.g., type Status = "pending" | "approved").

Modules and Namespaces

TypeScript supports both ES Modules (standard in JS) and namespaces (legacy TS feature for grouping code).

ES Modules

Use import/export to share code between files (same as modern JS):

// src/utils/math.ts  
export function add(a: number, b: number): number {  
  return a + b;  
}  

// src/index.ts  
import { add } from "./utils/math";  
console.log(add(2, 3));  // 5  

Namespaces

Namespaces group related code under a single global object (avoid for new projects; prefer ES modules):

namespace MathUtils {  
  export function multiply(a: number, b: number): number {  
    return a * b;  
  }  
}  

MathUtils.multiply(2, 3);  // 6  

Decorators

Decorators are functions that modify classes, methods, or properties. They’re widely used in frameworks like Angular and NestJS (experimental, but stable for most use cases).

Enabling Decorators

Add these to tsconfig.json:

{  
  "compilerOptions": {  
    "experimentalDecorators": true,  
    "emitDecoratorMetadata": true  
  }  
}  

Example: Class Decorator

Log when a class is instantiated:

function logClass(constructor: Function) {  
  console.log(`Class ${constructor.name} instantiated`);  
}  

@logClass  
class User {  
  constructor(public name: string) {}  
}  

const user = new User("Alice");  // Logs: "Class User instantiated"  

Migrating from JavaScript to TypeScript

Migrating incrementally is key. Here’s a step-by-step approach:

  1. Rename Files: Start with non-critical files (e.g., utilities) by renaming .js to .ts.
  2. Add Type Annotations: Start with function parameters and return types.
    // Before (JS)  
    function calculateTotal(items) {  
      return items.reduce((sum, item) => sum + item.price, 0);  
    }  
    
    // After (TS)  
    type CartItem = { price: number };  
    function calculateTotal(items: CartItem[]): number {  
      return items.reduce((sum, item) => sum + item.price, 0);  
    }  
  3. Use any Sparingly: Temporarily use any for untyped dependencies, but replace with proper types later.
  4. Leverage // @ts-ignore: Suppress errors during migration (but document why!).

Tooling and Debugging

IDE Support

VS Code has built-in TypeScript support, including:

  • Autocompletion based on types.
  • Inline error highlighting.
  • Refactoring (rename symbols, extract functions).

Debugging

Enable sourceMap: true in tsconfig.json to map compiled JS back to TS. Debug in VS Code or browsers using source maps.

Conclusion

TypeScript transforms JavaScript development by adding type safety, improving tooling, and making code more maintainable. By mastering its core concepts—types, interfaces, generics, and utilities—you’ll write more robust applications with fewer bugs. Start small, experiment, and gradually adopt TypeScript in your projects—your future self (and team) will thank you!

References