TypeScript Best Practices: Writing Type-Safe JavaScript Code
TypeScript has revolutionized JavaScript development by adding static typing to the language. It helps catch errors at compile time, provides better IDE support, and makes code more maintainable.
TypeScript offers several advantages over plain JavaScript:
Start with the fundamentals of TypeScript types:
// Primitive types
let name: string = 'John';
let age: number = 30;
let isActive: boolean = true;
let hobbies: string[] = ['reading', 'coding'];
// Object types
interface User {
id: number;
name: string;
email: string;
age?: number; // Optional property
}
const user: User = {
id: 1,
name: 'John Doe',
email: 'john@example.com'
};TypeScript provides powerful function typing capabilities:
// Function with typed parameters and return
function add(a: number, b: number): number {
return a + b;
}
// Arrow function with types
const multiply = (a: number, b: number): number => a * b;
// Function with optional parameters
function greet(name: string, greeting?: string): string {
return greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`;
}
// Function with rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}Use interfaces and types to define contracts:
// Interface for API response
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// Type alias for union types
type Status = 'loading' | 'success' | 'error';
// Interface extending another interface
interface Employee extends User {
department: string;
salary: number;
}
// Interface with methods
interface Calculator {
add(a: number, b: number): number;
subtract(a: number, b: number): number;
}Generics provide type safety while maintaining flexibility:
// 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];
}
}Combine types for more flexible type definitions:
// Union types
type StringOrNumber = string | number;
type Status = 'idle' | 'loading' | 'success' | 'error';
// Intersection types
interface HasName {
name: string;
}
interface HasAge {
age: number;
}
type Person = HasName & HasAge;
// Type guards
function isString(value: StringOrNumber): value is string {
return typeof value === 'string';
}
function processValue(value: StringOrNumber): string {
if (isString(value)) {
return value.toUpperCase();
} else {
return value.toString();
}
}// Make all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Make all properties required
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Make all properties readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Example usage
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
type RequiredUser = Required<User>;
type ReadonlyUser = Readonly<User>;// Conditional type
type NonNullable<T> = T extends null | undefined ? never : T;
// Extract return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// Extract parameter types
type Parameters<T> = T extends (...args: infer P) => any ? P : never;Enable strict mode in your tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}// Good
interface User {
id: number;
name: string;
}
// Avoid
type User = {
id: number;
name: string;
};// Good - when you know more than TypeScript
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
// Better - type guards
function isCanvas(element: HTMLElement): element is HTMLCanvasElement {
return element.tagName === 'CANVAS';
}interface User {
id: number;
name: string;
email: string;
password: string;
}
// Omit sensitive data
type PublicUser = Omit<User, 'password'>;
// Pick specific properties
type UserCredentials = Pick<User, 'email' | 'password'>;
// Make some properties optional
type UpdateUser = Partial<Pick<User, 'name' | 'email'>>;enum UserRole {
ADMIN = 'admin',
USER = 'user',
MODERATOR = 'moderator'
}
enum HttpStatus {
OK = 200,
CREATED = 201,
BAD_REQUEST = 400,
UNAUTHORIZED = 401
}TypeScript helps with better error handling:
// Result type for error handling
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// Async result
async function fetchUser(id: number): Promise<Result<User>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const user = await response.json();
return { success: true, data: user };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Unknown error')
};
}
}TypeScript works excellently with React:
import React, { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
}
interface UserListProps {
users: User[];
onUserSelect: (user: User) => void;
}
const UserList: React.FC<UserListProps> = ({ users, onUserSelect }) => {
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const handleUserClick = (user: User) => {
setSelectedUser(user);
onUserSelect(user);
};
return (
<ul>
{users.map(user => (
<li
key={user.id}
onClick={() => handleUserClick(user)}
className={selectedUser?.id === user.id ? 'selected' : ''}
>
{user.name}
</li>
))}
</ul>
);
};TypeScript improves testing experience:
import { describe, it, expect } from 'vitest';
interface Calculator {
add(a: number, b: number): number;
subtract(a: number, b: number): number;
}
class SimpleCalculator implements Calculator {
add(a: number, b: number): number {
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
}
describe('Calculator', () => {
let calculator: Calculator;
beforeEach(() => {
calculator = new SimpleCalculator();
});
it('should add two numbers correctly', () => {
expect(calculator.add(2, 3)).toBe(5);
});
it('should subtract two numbers correctly', () => {
expect(calculator.subtract(5, 3)).toBe(2);
});
});// Good - simple and clear
function map<T, U>(array: T[], fn: (item: T) => U): U[] {
return array.map(fn);
}
// Avoid - overly complex
function complexGeneric<
T extends object,
K extends keyof T,
V extends T[K],
R extends Record<K, V>
>(obj: T, key: K, value: V): R {
return { ...obj, [key]: value } as R;
}// Good - more specific types
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number];
// Avoid - less specific
const colors = ['red', 'green', 'blue']; // string[]TypeScript is a powerful tool that significantly improves JavaScript development. By following these best practices, you can write more maintainable, type-safe code that scales with your project.
Remember to:
TypeScript is an investment that pays off in the long run with better code quality and developer experience.