🎯 Why TypeScript?#
TypeScript is a strongly typed superset of JavaScript that compiles to plain JavaScript. It provides static type checking, better IDE support, and helps catch errors at compile time rather than runtime.
✨ Key Benefits#
- Type Safety: Catch errors before they reach production
- Better IDE Support: Enhanced autocomplete and refactoring
- Self-Documenting Code: Types serve as documentation
- Easier Refactoring: Confident code changes with type checking
🏗️ Type System Fundamentals#
1. Basic Types#
// Primitive types
let name: string = "John";
let age: number = 30;
let isActive: boolean = true;
let items: string[] = ["apple", "banana"];
let user: { name: string; age: number } = { name: "John", age: 30 };
// Union types
let id: string | number = "123";
let status: "pending" | "approved" | "rejected" = "pending";
// Optional properties
interface User {
name: string;
age?: number; // Optional
email: string;
}
2. Interfaces vs Type Aliases#
// Use interfaces for object shapes
interface User {
id: number;
name: string;
email: string;
isActive?: boolean;
}
// Use type aliases for unions and complex types
type Status = "pending" | "approved" | "rejected";
type UserWithRole = User & { role: "admin" | "user" };
type EventHandler = (event: Event) => void;
🔧 Advanced Type Features#
1. Generics#
// Generic function
function identity<T>(arg: T): T {
return arg;
}
// Generic interface
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// Generic constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Usage
const user = { name: "John", age: 30 };
const name = getProperty(user, "name"); // string
const age = getProperty(user, "age"); // number
2. Utility Types#
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Built-in utility types
type PartialUser = Partial<User>; // All properties optional
type RequiredUser = Required<User>; // All properties required
type UserName = Pick<User, "name" | "email">; // Pick specific properties
type UserWithoutId = Omit<User, "id">; // Exclude specific properties
type ReadonlyUser = Readonly<User>; // All properties readonly
// Custom utility types
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
🛡️ Type Safety Patterns#
1. Type Guards#
// Type predicate
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isUser(obj: any): obj is User {
return obj && typeof obj.name === "string" && typeof obj.email === "string";
}
// Usage
function processValue(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
}
2. Discriminated Unions#
type LoadingState = {
status: "loading";
};
type SuccessState = {
status: "success";
data: User[];
};
type ErrorState = {
status: "error";
error: string;
};
type ApiState = LoadingState | SuccessState | ErrorState;
function handleApiState(state: ApiState) {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
return `Loaded ${state.data.length} users`;
case "error":
return `Error: ${state.error}`;
}
}
🎨 Code Organization#
1. Module Structure#
// types/user.ts
export interface User {
id: number;
name: string;
email: string;
}
export type CreateUserRequest = Omit<User, "id">;
export type UpdateUserRequest = Partial<CreateUserRequest>;
// services/userService.ts
import { User, CreateUserRequest, UpdateUserRequest } from "../types/user";
export class UserService {
async getUsers(): Promise<User[]> {
// Implementation
}
async createUser(user: CreateUserRequest): Promise<User> {
// Implementation
}
async updateUser(id: number, user: UpdateUserRequest): Promise<User> {
// Implementation
}
}
2. Error Handling#
// Result type pattern
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: number): Promise<Result<User>> {
try {
const user = await api.getUser(id);
return { success: true, data: user };
} catch (error) {
return { success: false, error: error as Error };
}
}
// Usage
const result = await fetchUser(1);
if (result.success) {
console.log(result.data.name);
} else {
console.error(result.error.message);
}
🚀 Performance Considerations#
1. Type Inference#
// Let TypeScript infer types when possible
const users = ["John", "Jane", "Bob"]; // string[]
const user = { name: "John", age: 30 }; // { name: string; age: number }
// Only add explicit types when necessary
const apiUrl: string = process.env.API_URL || "http://localhost:3000";
2. Conditional Types#
type NonNullable<T> = T extends null | undefined ? never : T;
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// Advanced conditional types
type ApiEndpoint<T> = T extends "users"
? User[]
: T extends "posts"
? Post[]
: never;
function fetchData<T extends "users" | "posts">(endpoint: T): Promise<ApiEndpoint<T>> {
// Implementation
}
📝 Best Practices#
1. Configuration#
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
}
}
2. Code Guidelines#
- Use strict mode - Enable all strict type checking options
- Prefer interfaces over types - For object shapes, use interfaces
- Use meaningful names -
UserRepository
instead ofRepo
- Avoid
any
- Useunknown
or specific types instead - Use enums for constants -
enum Status { Pending, Approved, Rejected }
- Document complex types - Add JSDoc comments for complex type definitions
3. Testing with Types#
// Type testing
type Assert<T extends true> = T;
type IsString<T> = T extends string ? true : false;
// These will cause compile errors if types don't match
type Test1 = Assert<IsString<"hello">>; // ✅
type Test2 = Assert<IsString<123>>; // ❌ Compile error
// Runtime type checking
function assertIsUser(obj: any): asserts obj is User {
if (!obj || typeof obj.name !== "string" || typeof obj.email !== "string") {
throw new Error("Invalid user object");
}
}
🎯 Common Patterns#
1. Builder Pattern#
class QueryBuilder<T> {
private query: Partial<T> = {};
where<K extends keyof T>(key: K, value: T[K]): this {
this.query[key] = value;
return this;
}
build(): Partial<T> {
return { ...this.query };
}
}
// Usage
const userQuery = new QueryBuilder<User>()
.where("name", "John")
.where("isActive", true)
.build();
2. Factory Pattern#
type UserRole = "admin" | "user" | "guest";
interface UserFactory {
createUser(role: UserRole): User;
}
class ConcreteUserFactory implements UserFactory {
createUser(role: UserRole): User {
const baseUser = { id: Date.now(), email: "" };
switch (role) {
case "admin":
return { ...baseUser, name: "Admin", permissions: ["read", "write", "delete"] };
case "user":
return { ...baseUser, name: "User", permissions: ["read"] };
case "guest":
return { ...baseUser, name: "Guest", permissions: [] };
}
}
}
🎉 Conclusion#
TypeScript is a powerful tool that can significantly improve your development experience and code quality. By following these best practices and patterns, you’ll write more maintainable, scalable, and error-free applications.
Remember to:
- Start with strict mode enabled
- Use types to document your code
- Leverage TypeScript’s advanced features like generics and utility types
- Write tests that verify your types
- Refactor confidently with type safety
Happy typing with TypeScript! 🚀