
Good TypeScript Practices
Published on July 4, 2025
Embrace the Power of the "unknown"
The unknown
type provides a type-safe approach to handling values of indeterminate types. It forces explicit type checking before performing operations. This prevents runtime errors that any
might silently allow. The any
type, in contrast, removes all type checking for a variable. While it avoids compile-time warnings, it allows you to perform any operation on the variable without TypeScript's scrutiny, potentially leading to hard-to-debug runtime errors when the data's actual type does not match the assumed operation.** unknown
ensures you always validate the type before interacting with the value in a type-specific way.
Scenario: A function receives data from an external API. The data's structure isn't guaranteed. Using unknown
ensures you validate the data before processing.
function processApiResponse(data: unknown) {
if (typeof data === "object" && data !== null && "message" in data) {
// TypeScript now knows 'data' has a 'message' property
console.log((data as { message: string }).message);
} else {
console.error("API response lacks 'message' property or is not an object.");
}
}
processApiResponse({ message: "Data received!" });
processApiResponse(123); // Will trigger the error log
Strategic Type Definitions: "type", "interface", and "enum"
TypeScript offers distinct tools for type definition, each serving a unique purpose.
Naming Constant Values
Enums provide a clear, readable way to define named constant values. They enhance code clarity when working with a fixed set of options.
Scenario: Represent the different states of an order in an order management system.
enum OrderStatus {
Pending = 'PENDING',
Processing = 'PROCESSING',
Shipped = 'SHIPPED',
Delivered = 'DELIVERED',
Cancelled = 'CANCELLED',
}
let currentOrderState: OrderStatus = OrderStatus.Processing;
if (currentOrderState === OrderStatus.Processing) {
console.log("Order is currently being prepared for shipment.");
}
Shaping Object Structures
Interfaces define the shape and contract of an object. They excel at enforcing consistency across objects, ensuring specific properties and methods exist.
Scenario: Define a contract for a Product
object used throughout an e-commerce application.
interface Product {
id: string;
name: string;
price: number;
description?: string; // Optional property
}
const laptop: Product = {
id: 'abc-123',
name: 'Super Laptop X',
price: 1200.00,
description: 'High-performance laptop for demanding tasks.',
};
const keyboard: Product = {
id: 'def-456',
name: 'Mechanical Keyboard',
price: 99.99,
};
console.log(laptop);
console.log(keyboard);
Crafting Flexible Aliases
The type
keyword creates aliases for primitive types, unions, intersections, tuples, and other complex structures. It offers great flexibility for defining complex data relationships.
Scenario: Define a complex configuration object that can have various forms.
type ThemeColor = 'light' | 'dark' | 'system';
type UserPreferences = {
theme: ThemeColor;
notificationsEnabled: boolean;
language: string;
};
const userSettings: UserPreferences = {
theme: 'dark',
notificationsEnabled: true,
language: 'en-US',
};
console.log(userSettings);
Fortify with Type Guards and Optional Properties
Robust applications anticipate missing data. TypeScript offers powerful mechanisms to handle null
or undefined
values gracefully.
Type Guards for Runtime Checks
Type guards ensure variables hold the expected type before operations. This prevents runtime errors that arise from operations on incorrect types.
Scenario: A function processes user input, which might be a string or null
. A type guard validates its presence.
function greetUser(username: string | null) {
if (username) { // This acts as a type guard, narrowing 'username' to 'string'
console.log(`Hello, ${username.toUpperCase()}!`);
} else {
console.log("Hello, Guest!");
}
}
greetUser("Alice");
greetUser(null);
Optional Properties
The Partial<T>
utility type constructs a type where all properties of T
are optional. This proves immensely useful when creating objects with some but not all properties of a base type.
Scenario: An updateUser
function receives only the fields that need modification.
interface UserProfile {
id: string;
firstName: string;
lastName: string;
email: string;
phoneNumber?: string;
}
function updateUser(userId: string, updates: Partial<UserProfile>) {
// In a real application, you would fetch the user, apply updates, and save.
console.log(`Updating user ${userId} with:`, updates);
}
// Example update: only changing the email
updateUser('user-123', { email: 'new.email@example.com' });
// Example update: changing first name and phone number
updateUser('user-456', { firstName: 'Bob', phoneNumber: '555-123-4567' });
Conclusion
By integrating these practices, you elevate your TypeScript code from functional to truly exceptional. Your applications gain clarity, stability, and maintainability. Embrace unknown
for safer external data, select type
, interface
, or enum
for precise definitions, and use type guards and Partial<T>
to build resilient systems. These strategies lead to more robust, understandable, and ultimately more enjoyable development experiences.