TypeScript Mastery: Complete Guide to Modern TypeScript Development
TypeScript has become the de facto standard for large-scale JavaScript development. With its powerful type system, enhanced tooling, and growing ecosystem, TypeScript is essential for modern web development. This comprehensive guide will take you from basics to advanced concepts.
What is TypeScript?
TypeScript is a superset of JavaScript that adds optional static typing, classes, and modules. It compiles to clean, simple JavaScript code that runs on any browser, in Node.js, or in any JavaScript engine.
Why TypeScript in 2024?
Key Benefits:
- Static Type Checking: Catch errors at compile time
- Better IDE Support: Enhanced autocomplete and refactoring
- Improved Maintainability: Self-documenting code
- Enhanced Team Collaboration: Clear interfaces and contracts
- Future-Proof: Access to latest JavaScript features
Getting Started with TypeScript
Installation
# Global installation
npm install -g typescript
# Local installation
npm install --save-dev typescript
# Initialize TypeScript project
npx tsc --init
Basic Configuration (tsconfig.json)
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "DOM"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
TypeScript Fundamentals
1. Basic Types
// Primitive types
let name: string = "John";
let age: number = 25;
let isActive: boolean = true;
let nothing: null = null;
let undefinedValue: undefined = undefined;
// Symbol type
let uniqueId: symbol = Symbol("id");
// BigInt type
let bigNumber: bigint = 100n;
2. Object Types
// Object type annotation
let person: {
name: string;
age: number;
email?: string; // Optional property
} = {
name: "John",
age: 25
};
// Interface definition
interface User {
id: number;
name: string;
email: string;
isActive?: boolean;
readonly createdAt: Date;
}
// Type alias
type Point = {
x: number;
y: number;
};
// Union types
type Status = "pending" | "approved" | "rejected";
type ID = string | number;
3. Array Types
// Array type annotations
let numbers: number[] = [1, 2, 3, 4, 5];
let names: Array<string> = ["John", "Jane", "Bob"];
// Tuple types
let coordinates: [number, number] = [10, 20];
let userInfo: [string, number, boolean] = ["John", 25, true];
// Readonly arrays
let readonlyNumbers: readonly number[] = [1, 2, 3];
4. Function Types
// Function type annotations
function add(a: number, b: number): number {
return a + b;
}
// Arrow function with types
const multiply = (a: number, b: number): number => a * b;
// Function type interface
interface MathFunc {
(x: number, y: number): number;
}
const divide: MathFunc = (a, b) => a / b;
// Optional and default parameters
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}!`;
}
// Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
Advanced TypeScript Features
1. Generics
// Generic function
function identity<T>(arg: T): T {
return arg;
}
// Generic interface
interface Container<T> {
value: T;
getValue(): T;
}
// Generic class
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
// Generic constraints
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
2. Union and Intersection Types
// Union types
type StringOrNumber = string | number;
type Status = "loading" | "success" | "error";
// Intersection types
interface HasName {
name: string;
}
interface HasAge {
age: number;
}
type Person = HasName & HasAge;
// Discriminated unions
interface LoadingState {
status: "loading";
}
interface SuccessState {
status: "success";
data: any;
}
interface ErrorState {
status: "error";
error: string;
}
type RequestState = LoadingState | SuccessState | ErrorState;
3. Mapped Types
// Basic mapped type
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Partial type
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Pick type
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Record type
type Record<K extends keyof any, T> = {
[P in K]: T;
};
// Practical example
interface User {
id: number;
name: string;
email: string;
}
type UserUpdate = Partial<User>;
type UserDisplay = Pick<User, "name" | "email">;
4. Conditional Types
// Basic conditional type
type NonNullable<T> = T extends null | undefined ? never : T;
// Type inference with conditional types
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
// Practical example
type StringOrNumberArray = ToArray<string | number>; // string[] | number[]
// Advanced conditional types
type IsString<T> = T extends string ? true : false;
type IsNumber<T> = T extends number ? true : false;
5. Template Literal Types
// Basic template literal types
type Greeting = `Hello ${string}`;
type Email = `${string}@${string}.${string}`;
// Advanced template literal types
type EventName<T extends string> = `${T}Changed`;
type PropEventSource<T> = {
on(eventName: EventName<keyof T>, callback: (newValue: any) => void): void;
};
// Practical example
interface User {
name: string;
age: number;
}
type UserEvents = PropEventSource<User>;
TypeScript with React
1. Component Types
import React, { useState, useEffect } from 'react';
// Props interface
interface UserCardProps {
user: {
id: number;
name: string;
email: string;
avatar?: string;
};
onEdit?: (user: UserCardProps['user']) => void;
onDelete?: (id: number) => void;
}
// Functional component with TypeScript
const UserCard: React.FC<UserCardProps> = ({ user, onEdit, onDelete }) => {
const [isEditing, setIsEditing] = useState(false);
const handleEdit = () => {
if (onEdit) {
onEdit(user);
}
};
const handleDelete = () => {
if (onDelete) {
onDelete(user.id);
}
};
return (
<div className="user-card">
<img src={user.avatar || '/default-avatar.png'} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
<div className="actions">
<button onClick={handleEdit}>Edit</button>
<button onClick={handleDelete}>Delete</button>
</div>
</div>
);
};
2. Hook Types
// Custom hook with TypeScript
interface UseCounterOptions {
initialValue?: number;
step?: number;
min?: number;
max?: number;
}
interface UseCounterReturn {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
setCount: (value: number) => void;
}
function useCounter(options: UseCounterOptions = {}): UseCounterReturn {
const {
initialValue = 0,
step = 1,
min = -Infinity,
max = Infinity
} = options;
const [count, setCount] = useState(initialValue);
const increment = () => {
setCount(prev => Math.min(prev + step, max));
};
const decrement = () => {
setCount(prev => Math.max(prev - step, min));
};
const reset = () => {
setCount(initialValue);
};
return {
count,
increment,
decrement,
reset,
setCount
};
}
3. Context Types
// Context with TypeScript
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = React.createContext<ThemeContextType | undefined>(undefined);
// Provider component (example for demonstration)
// In real applications, use a proper theme management solution
// Custom hook for context
// Note: This is just an example for the blog post
// In real applications, use the actual useTheme hook from ThemeProvider
Advanced Patterns
1. Builder Pattern
class QueryBuilder<T> {
private query: Partial<T> = {};
private conditions: Array<{ field: keyof T; operator: string; value: any }> = [];
where<K extends keyof T>(field: K, operator: string, value: T[K]): this {
this.conditions.push({ field, operator, value });
return this;
}
select<K extends keyof T>(fields: K[]): this {
this.query = fields.reduce((obj, field) => {
obj[field] = undefined as any;
return obj;
}, {} as Partial<T>);
return this;
}
build(): { query: Partial<T>; conditions: Array<{ field: keyof T; operator: string; value: any }> } {
return { query: this.query, conditions: this.conditions };
}
}
// Usage
interface User {
id: number;
name: string;
email: string;
age: number;
}
const query = new QueryBuilder<User>()
.select(['name', 'email'])
.where('age', '>', 18)
.where('name', 'LIKE', 'John%')
.build();
2. Factory Pattern
interface Animal {
name: string;
makeSound(): string;
}
class Dog implements Animal {
constructor(public name: string) {}
makeSound(): string {
return 'Woof!';
}
}
class Cat implements Animal {
constructor(public name: string) {}
makeSound(): string {
return 'Meow!';
}
}
type AnimalType = 'dog' | 'cat';
class AnimalFactory {
static createAnimal(type: AnimalType, name: string): Animal {
switch (type) {
case 'dog':
return new Dog(name);
case 'cat':
return new Cat(name);
default:
throw new Error(`Unknown animal type: ${type}`);
}
}
}
// Usage
const dog = AnimalFactory.createAnimal('dog', 'Buddy');
const cat = AnimalFactory.createAnimal('cat', 'Whiskers');
3. Observer Pattern
interface Observer<T> {
update(data: T): void;
}
interface Subject<T> {
attach(observer: Observer<T>): void;
detach(observer: Observer<T>): void;
notify(data: T): void;
}
class EventEmitter<T> implements Subject<T> {
private observers: Observer<T>[] = [];
attach(observer: Observer<T>): void {
this.observers.push(observer);
}
detach(observer: Observer<T>): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(data: T): void {
this.observers.forEach(observer => observer.update(data));
}
}
// Usage
class Logger implements Observer<string> {
update(data: string): void {
console.log(`Log: ${data}`);
}
}
const emitter = new EventEmitter<string>();
const logger = new Logger();
emitter.attach(logger);
emitter.notify('Hello, World!');
TypeScript Best Practices
1. Type Safety
// Use strict mode
// tsconfig.json: "strict": true
// Avoid any type
// Bad
function processData(data: any): any {
return data;
}
// Good
function processData<T>(data: T): T {
return data;
}
// Use type guards
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function processValue(value: unknown): string {
if (isString(value)) {
return value.toUpperCase();
}
throw new Error('Value is not a string');
}
2. Error Handling
// Custom error types
class ValidationError extends Error {
constructor(
message: string,
public field: string,
public value: any
) {
super(message);
this.name = 'ValidationError';
}
}
// Result type for error handling
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function divide(a: number, b: number): Result<number, ValidationError> {
if (b === 0) {
return {
success: false,
error: new ValidationError('Division by zero', 'b', b)
};
}
return {
success: true,
data: a / b
};
}
3. Performance Optimization
// Use const assertions
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number];
// Use readonly for immutable data
interface Config {
readonly apiUrl: string;
readonly timeout: number;
}
// Use branded types for type safety
type UserId = string & { readonly brand: unique symbol };
type ProductId = string & { readonly brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}
function createProductId(id: string): ProductId {
return id as ProductId;
}
Testing with TypeScript
1. Jest Configuration
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
};
// Example test
import { Calculator } from './Calculator';
describe('Calculator', () => {
let calculator: Calculator;
beforeEach(() => {
calculator = new Calculator();
});
test('should add two numbers correctly', () => {
expect(calculator.add(2, 3)).toBe(5);
});
test('should throw error for division by zero', () => {
expect(() => calculator.divide(10, 0)).toThrow('Division by zero');
});
});
2. Mock Types
// Mock interface
interface UserService {
getUser(id: string): Promise<User>;
createUser(user: Omit<User, 'id'>): Promise<User>;
}
// Mock implementation
const mockUserService: jest.Mocked<UserService> = {
getUser: jest.fn(),
createUser: jest.fn(),
};
// Usage in tests
beforeEach(() => {
jest.clearAllMocks();
mockUserService.getUser.mockResolvedValue({
id: '1',
name: 'John Doe',
email: 'john@example.com'
});
});
Conclusion
TypeScript is a powerful tool that enhances JavaScript development with type safety, better tooling, and improved maintainability. By mastering TypeScript, you can write more robust, scalable applications.
The key to TypeScript mastery is practice and understanding the type system. Start with basic types and gradually work your way up to advanced features like generics, conditional types, and mapped types.
Remember that TypeScript is designed to help you write better code, not to make your code more complex. Use types to express intent and catch errors early in development.
As you continue your TypeScript journey, explore the ecosystem of libraries and tools that leverage TypeScript's type system to provide better developer experience and runtime safety.
Happy coding with TypeScript! 🚀