As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
Working with TypeScript feels like having a dedicated partner in the development process. It doesn't just point out mistakes—it helps you design systems that are inherently more reliable. Over time, I've found that certain patterns consistently lead to cleaner, more resilient code. They transform the type system from a passive checker into an active design tool.
One of the most powerful concepts is conditional types. They allow types to change shape based on conditions you define. Imagine building an API where some responses include sensitive data and others don't. Instead of creating multiple interfaces, you can use a conditional type that adapts to the situation.
type UserResponse<T extends boolean> = T extends true
? { id: string; name: string; ssn: string }
: { id: string; name: string };
async function fetchUser<T extends boolean>(
includePrivate: T
): Promise<UserResponse<T>> {
const response = await fetch('/api/user');
const data = await response.json();
if (!includePrivate) {
delete data.ssn;
}
return data as UserResponse<T>;
}
// The return type changes based on the boolean parameter
const publicUser = await fetchUser(false); // { id: string; name: string }
const privateUser = await fetchUser(true); // { id: string; name: string; ssn: string }
This approach eliminates entire categories of potential errors. The compiler ensures you never accidentally access private fields when they shouldn't be available.
Template literal types bring precision to string handling. I've used them to validate routes, identifiers, and formatting patterns that would otherwise require runtime checks.
type APIEndpoint = `/api/${string}/${string}`;
type CSSHexColor = `#${string}`;
function validateEndpoint(path: APIEndpoint) {
// TypeScript guarantees this starts with /api/
}
function setColor(color: CSSHexColor) {
// Always receives a valid hex format
}
// These will cause compile errors:
validateEndpoint('/users'); // Missing /api prefix
setColor('red'); // Not hex format
The compiler catches formatting mistakes before they can cause runtime issues. I've found this particularly valuable when working with third-party APIs that expect specific string formats.
Discriminated unions have become my go-to solution for handling different states and outcomes. They make state management explicit and error-proof.
type LoadableData<T> =
| { state: 'loading' }
| { state: 'success'; data: T }
| { state: 'error'; code: number; message: string };
function renderContent(content: LoadableData<UserProfile>) {
switch (content.state) {
case 'loading':
return <Spinner />;
case 'success':
return <Profile data={content.data} />; // content.data is known to exist
case 'error':
return <ErrorMessage code={content.code} message={content.message} />;
}
}
This pattern eliminates null checks and undefined access. The type system guides you through handling every possible state. I've used this for API responses, UI states, and even complex multi-step processes.
Mapped types provide incredible flexibility when working with existing type definitions. They help create variations without duplication.
interface DatabaseUser {
id: string;
email: string;
createdAt: Date;
updatedAt: Date;
}
// Create types for different operations
type UserCreate = Omit<DatabaseUser, 'id' | 'createdAt' | 'updatedAt'>;
type UserUpdate = Partial<Omit<DatabaseUser, 'id' | 'createdAt'>>;
type UserResponse = Pick<DatabaseUser, 'id' | 'email'>;
// Custom mapped type for validation
type ValidationRules<T> = {
[K in keyof T]: (value: T[K]) => boolean;
};
const userValidation: ValidationRules<UserCreate> = {
email: (value) => value.includes('@'),
// TypeScript ensures we implement validators for all required fields
};
This approach keeps related types synchronized. When the base interface changes, the derived types update automatically. I've avoided countless bugs by using mapped types instead of manually maintaining separate interfaces.
Branded types solve a common problem in TypeScript's structural type system. Different kinds of identifiers might all be strings, but they represent distinct concepts.
type UserID = string & { readonly __brand: 'UserID' };
type OrderID = string & { readonly __brand: 'OrderID' };
function createUserID(id: string): UserID {
if (!id.startsWith('user_')) {
throw new Error('Invalid user ID format');
}
return id as UserID;
}
function getOrder(id: OrderID) {
// Only accepts OrderID, not any string
}
const userId = createUserID('user_123');
const orderId = 'order_456' as OrderID;
getOrder(userId); // Compile error: UserID is not assignable to OrderID
This pattern has prevented numerous bugs in my projects. The compiler now catches cases where I might accidentally pass the wrong type of identifier to a function.
Generic constraints ensure functions work with appropriate types while maintaining flexibility.
interface Entity {
id: string;
}
function sortById<T extends Entity>(entities: T[]): T[] {
return entities.sort((a, b) => a.id.localeCompare(b.id));
}
// Works with any type that has an id property
const users = sortById([{ id: '2', name: 'Bob' }, { id: '1', name: 'Alice' }]);
const products = sortById([{ id: 'p2', price: 100 }, { id: 'p1', price: 50 }]);
// Fails compile check - numbers don't have id property
sortById([1, 2, 3]);
The constraints provide just enough information for the function to work correctly while allowing maximum reuse. I use this pattern extensively in utility functions and shared libraries.
Type predicates allow for custom type narrowing logic. They're essential when working with complex validation scenarios.
interface ApiUser {
id: string;
name: string;
permissions?: string[];
}
function hasAdminAccess(user: ApiUser): user is ApiUser & { permissions: string[] } {
return user.permissions?.includes('admin') ?? false;
}
async function processUser(user: ApiUser) {
if (hasAdminAccess(user)) {
// TypeScript now knows user.permissions exists
await grantAdminPrivileges(user.permissions);
} else {
// Handle regular user
}
}
This pattern makes complex validation logic reusable and type-safe. The compiler understands the implications of your validation functions and adjusts types accordingly.
These patterns work together to create a development experience where the type system actively helps you write better code. They catch mistakes early, guide implementation, and make intentions explicit. The result is code that's more maintainable, self-documenting, and robust against entire categories of errors.
The true value emerges when these patterns combine. A discriminated union might use branded types for identifiers. A generic function might employ type predicates for validation. Mapped types might generate variations of interfaces used throughout the application.
This approach changes how I think about development. Instead of writing code and adding types later, the type design becomes part of the initial planning process. The patterns help create a structure that guides implementation and prevents whole classes of errors from ever occurring.
The feedback loop becomes incredibly tight. Mistakes surface immediately during development rather than appearing during testing or production. This speeds up development while simultaneously improving quality. The type system becomes less about restriction and more about empowerment—it helps you build better systems with confidence.
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (0)