Mastering TypeScript: From Basics to Advanced
A comprehensive guide to TypeScript covering type annotations, interfaces, generics, and advanced patterns for building type-safe applications.
Mastering TypeScript: From Basics to Advanced
TypeScript has revolutionized how we write JavaScript by adding static type checking, making our code more robust and maintainable. This comprehensive guide will take you from TypeScript basics to advanced patterns.
Why TypeScript?
TypeScript offers several compelling benefits:
- Type Safety: Catch errors at compile time
- Better IDE Support: Improved autocomplete and refactoring
- Enhanced Readability: Types serve as documentation
- Modern JavaScript: Use latest ECMAScript features
- Scalability: Easier to maintain large codebases
Basic Types
Let's start with the fundamental types:
// Primitive types
let name: string = "John";
let age: number = 30;
let isActive: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;
// Arrays
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ["a", "b", "c"];
// Tuple
let tuple: [string, number] = ["hello", 42];
// Enum
enum Color {
Red,
Green,
Blue
}
let color: Color = Color.Red;
// Any (avoid when possible)
let anything: any = 42;
anything = "now a string";
// Unknown (safer than any)
let uncertain: unknown = 42;
if (typeof uncertain === "string") {
console.log(uncertain.toUpperCase());
}
Interfaces and Type Aliases
Define custom types for objects:
// Interface
interface User {
id: number;
name: string;
email: string;
age?: number; // Optional property
readonly createdAt: Date; // Read-only property
}
const user: User = {
id: 1,
name: "John Doe",
email: "john@example.com",
createdAt: new Date()
};
// Type alias
type Point = {
x: number;
y: number;
};
// Extending interfaces
interface Admin extends User {
role: "admin";
permissions: string[];
}
// Union types
type Status = "pending" | "approved" | "rejected";
let status: Status = "pending";
// Intersection types
type Timestamp = {
createdAt: Date;
updatedAt: Date;
};
type Article = User & Timestamp & {
title: string;
content: string;
};
Functions
Type-safe functions in TypeScript:
// Function with typed parameters and return type
function add(a: number, b: number): number {
return 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((acc, num) => acc + num, 0);
}
// Function type
type MathOperation = (a: number, b: number) => number;
const multiply: MathOperation = (a, b) => a * b;
// Void return type
function logMessage(message: string): void {
console.log(message);
}
// Never type (function that never returns)
function throwError(message: string): never {
throw new Error(message);
}
Generics
Write reusable, type-safe code:
// Generic function
function identity<T>(arg: T): T {
return arg;
}
const num = identity<number>(42);
const str = identity<string>("hello");
// Generic interface
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
const userResponse: ApiResponse<User> = {
data: { id: 1, name: "John", email: "john@example.com", createdAt: new Date() },
status: 200,
message: "Success"
};
// Generic class
class DataStore<T> {
private data: T[] = [];
add(item: T): void {
this.data.push(item);
}
get(index: number): T | undefined {
return this.data[index];
}
getAll(): T[] {
return [...this.data];
}
}
const userStore = new DataStore<User>();
userStore.add(user);
// Generic constraints
interface Lengthy {
length: number;
}
function logLength<T extends Lengthy>(item: T): void {
console.log(item.length);
}
logLength("hello"); // Works
logLength([1, 2, 3]); // Works
// logLength(42); // Error: number doesn't have length
Advanced Types
Utility Types
TypeScript provides built-in utility types:
// Partial - makes all properties optional
type PartialUser = Partial<User>;
// Required - makes all properties required
type RequiredUser = Required<User>;
// Readonly - makes all properties readonly
type ReadonlyUser = Readonly<User>;
// Pick - select specific properties
type UserPreview = Pick<User, "id" | "name">;
// Omit - exclude specific properties
type UserWithoutEmail = Omit<User, "email">;
// Record - create an object type with specific keys
type UserRoles = Record<string, "admin" | "user" | "guest">;
// Extract - extract types from union
type OnlyStrings = Extract<string | number | boolean, string>;
// Exclude - exclude types from union
type NoStrings = Exclude<string | number | boolean, string>;
Mapped Types
Transform types programmatically:
// Make all properties optional
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Transform property types
type Stringify<T> = {
[K in keyof T]: string;
};
Conditional Types
Types that depend on conditions:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Extract return type
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
type AddReturnType = ReturnTypeOf<typeof add>; // number
Best Practices
1. Prefer Interfaces for Object Shapes
// Good
interface User {
id: number;
name: string;
}
// Also good for unions/intersections
type Status = "active" | "inactive";
2. Use Strict Mode
Enable strict mode in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}
3. Avoid any
Use unknown instead and perform type checks:
// Bad
function process(data: any) {
return data.value;
}
// Good
function process(data: unknown) {
if (typeof data === "object" && data !== null && "value" in data) {
return (data as { value: any }).value;
}
throw new Error("Invalid data");
}
4. Use Type Guards
function isUser(obj: any): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
typeof obj.id === "number" &&
typeof obj.name === "string"
);
}
if (isUser(data)) {
// TypeScript knows data is User here
console.log(data.name);
}
5. Leverage Generics
Make code reusable while maintaining type safety:
function fetchData<T>(url: string): Promise<T> {
return fetch(url).then(res => res.json());
}
const users = await fetchData<User[]>("/api/users");
Common Patterns
Factory Pattern
interface Product {
id: number;
name: string;
price: number;
}
class ProductFactory {
static create(data: Omit<Product, "id">): Product {
return {
id: Math.random(),
...data
};
}
}
const product = ProductFactory.create({
name: "Laptop",
price: 999
});
Builder Pattern
class QueryBuilder<T> {
private filters: Partial<T> = {};
private sortField?: keyof T;
private limitValue?: number;
where(field: keyof T, value: any): this {
this.filters[field] = value;
return this;
}
sort(field: keyof T): this {
this.sortField = field;
return this;
}
limit(value: number): this {
this.limitValue = value;
return this;
}
build() {
return {
filters: this.filters,
sort: this.sortField,
limit: this.limitValue
};
}
}
const query = new QueryBuilder<User>()
.where("name", "John")
.sort("createdAt")
.limit(10)
.build();
Conclusion
TypeScript is a powerful tool that enhances JavaScript development with type safety and better tooling. By mastering these concepts, you'll write more maintainable and robust code.
Keep practicing, and don't be afraid to leverage TypeScript's advanced features to make your code more type-safe and expressive!
Resources
Related Guides
Getting Started with Next.js 14: A Complete Guide
Learn how to build modern web applications with Next.js 14, including App Router, Server Components, and best practices for performance and SEO.
Building a REST API with Node.js and Express
Learn how to build a production-ready RESTful API using Node.js, Express, and MongoDB with authentication, validation, and best practices.