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
- Introduction to TypeScript
- Setting Up TypeScript
- Core TypeScript Concepts
- Enums
- Modules and Namespaces
- Decorators
- Migrating from JavaScript to TypeScript
- Tooling and Debugging
- Conclusion
- 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
-
Create a new project folder and initialize
package.json:mkdir ts-handbook && cd ts-handbook npm init -y -
Create a TypeScript configuration file (
tsconfig.json). This file defines compiler options:tsc --init -
Key
tsconfig.jsonOptions:Option Purpose 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: truefor 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 } -
Write your first TypeScript file:
Createsrc/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! -
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:
| Type | Description | Example |
|---|---|---|
number | Integers, floats, hex, binary, or octal | const age: number = 25; |
string | Text, with optional template literals | const name: string = "Alice"; |
boolean | true or false | const isActive: boolean = true; |
null | Explicit absence of value (strict mode only) | const empty: null = null; |
undefined | Uninitialized value | const missing: undefined = undefined; |
bigint | Large integers (ES2020+) | const big: bigint = 100n; |
symbol | Unique, immutable values | const 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
interfacefor object shapes that may need extension (e.g., class hierarchies). - Use
typefor 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:
| Utility | Purpose | Example |
|---|---|---|
Partial<T> | Makes all properties of T optional | Partial<User> → { id?: number; name?: string } |
Required<T> | Makes all properties of T required | Required<Partial<User>> → User |
Readonly<T> | Makes all properties of T read-only | Readonly<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 T | Omit<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:
- Rename Files: Start with non-critical files (e.g., utilities) by renaming
.jsto.ts. - 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); } - Use
anySparingly: Temporarily useanyfor untyped dependencies, but replace with proper types later. - 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
- TypeScript Official Documentation
- TypeScript Playground (experiment with TS code)
- Utility Types Guide
- Migrating from JS to TS
- TypeScript Deep Dive (free online book)