TypeScript Tips and Tricks for Better Code Quality
Michael Rodriguez

Discover advanced TypeScript techniques that will help you write more robust, maintainable, and type-safe applications.
TypeScript has become an essential tool for modern JavaScript development, providing static type checking and enhanced developer experience. In this comprehensive guide, we’ll explore advanced TypeScript techniques that will elevate your code quality and help you build more robust applications.
Utility Types: Your Secret Weapons
TypeScript provides several built-in utility types that can transform existing types in useful ways.
Partial and Required
"text-[#66d9ef]">interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Make all properties optional
"text-[#66d9ef]">type PartialUser = Partial<User>;
"text-[#66d9ef]">class="text-[#75715e] italic">// { id?: number; name?: string; email?: string; avatar?: string; }
"text-[#66d9ef]">class="text-[#75715e] italic">// Make all properties required (including optional ones)
"text-[#66d9ef]">type RequiredUser = Required<User>;
"text-[#66d9ef]">class="text-[#75715e] italic">// { id: number; name: string; email: string; avatar: string; }
"text-[#66d9ef]">class="text-[#75715e] italic">// Practical example: Update user "text-[#66d9ef]">function
"text-[#66d9ef]">function updateUser(id: number, updates: Partial<User>): User {
"text-[#66d9ef]">const existingUser = getUserById(id);
"text-[#66d9ef]">return { ...existingUser, ...updates };
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Usage
updateUser(1, { name: "John Doe" }); "text-[#66d9ef]">class="text-[#75715e] italic">// Only update name
updateUser(1, { email: "john@example.com", avatar: "avatar.jpg" });
Pick and Omit
"text-[#66d9ef]">interface Product {
id: number;
name: string;
description: string;
price: number;
category: string;
inStock: boolean;
createdAt: Date;
updatedAt: Date;
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Pick specific properties
"text-[#66d9ef]">type ProductSummary = Pick<Product, 'id' | 'name' | 'price'>;
"text-[#66d9ef]">class="text-[#75715e] italic">// { id: number; name: string; price: number; }
"text-[#66d9ef]">class="text-[#75715e] italic">// Omit specific properties
"text-[#66d9ef]">type CreateProductInput = Omit<Product, 'id' | 'createdAt' | 'updatedAt'>;
"text-[#66d9ef]">class="text-[#75715e] italic">// { name: string; description: string; price: number; category: string; inStock: boolean; }
"text-[#66d9ef]">class="text-[#75715e] italic">// Practical example: API response types
"text-[#66d9ef]">type ProductListItem = Pick<Product, 'id' | 'name' | 'price' | 'inStock'>;
"text-[#66d9ef]">function getProductList(): Promise<ProductListItem[]> {
"text-[#66d9ef]">return fetch('/api/products')
.then(response => response.json());
}
Advanced Type Guards
Type guards help TypeScript understand the type of a variable at runtime.
"text-[#66d9ef]">class="text-[#75715e] italic">// Custom "text-[#66d9ef]">type guard functions
"text-[#66d9ef]">function isString(value: unknown): value is string {
"text-[#66d9ef]">return typeof value === 'string';
}
"text-[#66d9ef]">function isNumber(value: unknown): value is number {
"text-[#66d9ef]">return typeof value === 'number' && !isNaN(value);
}
"text-[#66d9ef]">class="text-[#75715e] italic">// User-defined "text-[#66d9ef]">type guard "text-[#66d9ef]">for objects
"text-[#66d9ef]">interface Dog {
name: string;
breed: string;
bark(): void;
}
"text-[#66d9ef]">interface Cat {
name: string;
breed: string;
meow(): void;
}
"text-[#66d9ef]">function isDog(pet: Dog | Cat): pet is Dog {
"text-[#66d9ef]">return 'bark' in pet;
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Usage with "text-[#66d9ef]">type narrowing
"text-[#66d9ef]">function handlePet(pet: Dog | Cat) {
console.log(`${pet.name} is a ${pet.breed}`);
"text-[#66d9ef]">if (isDog(pet)) {
pet.bark(); "text-[#66d9ef]">class="text-[#75715e] italic">// TypeScript knows pet is Dog
} "text-[#66d9ef]">else {
pet.meow(); "text-[#66d9ef]">class="text-[#75715e] italic">// TypeScript knows pet is Cat
}
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Discriminated unions
"text-[#66d9ef]">type ApiResponse<T> =
| { success: "text-[#ae81ff]">true; data: T }
| { success: "text-[#ae81ff]">false; error: string };
"text-[#66d9ef]">function handleApiResponse<T>(response: ApiResponse<T>) {
"text-[#66d9ef]">if (response.success) {
"text-[#66d9ef]">class="text-[#75715e] italic">// TypeScript knows response.data exists
console.log('Data:', response.data);
} "text-[#66d9ef]">else {
"text-[#66d9ef]">class="text-[#75715e] italic">// TypeScript knows response.error exists
console.error('Error:', response.error);
}
}
Generic Constraints and Conditional Types
Generics become even more powerful when combined with constraints and conditional types.
"text-[#66d9ef]">class="text-[#75715e] italic">// Generic constraints
"text-[#66d9ef]">interface Identifiable {
id: number;
}
"text-[#66d9ef]">function updateEntity<T "text-[#66d9ef]">extends Identifiable>(
entities: T[],
id: number,
updates: Partial<T>
): T[] {
"text-[#66d9ef]">return entities.map(entity =>
entity.id === id ? { ...entity, ...updates } : entity
);
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Conditional types
"text-[#66d9ef]">type ApiEndpoint<T> = T "text-[#66d9ef]">extends 'users'
? '/api/users'
: T "text-[#66d9ef]">extends 'products'
? '/api/products'
: T "text-[#66d9ef]">extends 'orders'
? '/api/orders'
: never;
"text-[#66d9ef]">class="text-[#75715e] italic">// Mapped types with conditional logic
"text-[#66d9ef]">type NonNullable<T> = T "text-[#66d9ef]">extends "text-[#ae81ff]">null | "text-[#ae81ff]">undefined ? never : T;
"text-[#66d9ef]">type RequiredKeys<T> = {
[K in keyof T]-?: T[K] "text-[#66d9ef]">extends "text-[#ae81ff]">undefined ? never : K;
}[keyof T];
"text-[#66d9ef]">type OptionalKeys<T> = {
[K in keyof T]-?: T[K] "text-[#66d9ef]">extends "text-[#ae81ff]">undefined ? K : never;
}[keyof T];
"text-[#66d9ef]">class="text-[#75715e] italic">// Advanced example: Create a "text-[#66d9ef]">type that makes specific keys optional
"text-[#66d9ef]">type MakeOptional<T, K "text-[#66d9ef]">extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
"text-[#66d9ef]">interface User {
id: number;
name: string;
email: string;
password: string;
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Make password optional "text-[#66d9ef]">for user updates
"text-[#66d9ef]">type UserUpdate = MakeOptional<User, 'password'>;
"text-[#66d9ef]">class="text-[#75715e] italic">// { id: number; name: string; email: string; password?: string; }
Template Literal Types
Template literal types allow you to create types using string templates.
"text-[#66d9ef]">class="text-[#75715e] italic">// Basic template literal types
"text-[#66d9ef]">type Greeting = `Hello ${string}`;
"text-[#66d9ef]">type PersonalGreeting = `Hello ${'John' | 'Jane' | 'Bob'}`;
"text-[#66d9ef]">class="text-[#75715e] italic">// Advanced usage with events
"text-[#66d9ef]">type EventType = 'click' | 'hover' | 'focus';
"text-[#66d9ef]">type ElementType = 'button' | 'input' | 'div';
"text-[#66d9ef]">type EventName = `${ElementType}:${EventType}`;
"text-[#66d9ef]">class="text-[#75715e] italic">// 'button:click' | 'button:hover' | 'button:focus' | 'input:click' | ...
"text-[#66d9ef]">class="text-[#75715e] italic">// Practical example: CSS-in-JS utilities
"text-[#66d9ef]">type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
"text-[#66d9ef]">type CSSValue<T "text-[#66d9ef]">extends string> = `${number}${T}`;
"text-[#66d9ef]">type Spacing = CSSValue<'px'> | CSSValue<'rem'> | CSSValue<'em'>;
"text-[#66d9ef]">class="text-[#75715e] italic">// '10px' | '1rem' | '2em' | etc.
"text-[#66d9ef]">class="text-[#75715e] italic">// API route types
"text-[#66d9ef]">type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
"text-[#66d9ef]">type ApiRoute = `/api/${string}`;
"text-[#66d9ef]">type ApiEndpointWithMethod = `${HttpMethod} ${ApiRoute}`;
"text-[#66d9ef]">class="text-[#75715e] italic">// Example usage in route handler
"text-[#66d9ef]">function handleApiRequest(endpoint: ApiEndpointWithMethod) {
"text-[#66d9ef]">const [method, route] = endpoint.split(' ') as [HttpMethod, ApiRoute];
console.log(`Handling ${method} request to ${route}`);
}
Strict Configuration Tips
Configure TypeScript for maximum type safety and better developer experience.
// tsconfig.json - Recommended strict settings
{
"compilerOptions": {
"strict": true, // Enable all strict checks
"noImplicitAny": true, // Error on implicit any
"strictNullChecks": true, // Enable strict null checks
"strictFunctionTypes": true, // Strict function types
"noImplicitReturns": true, // Error on missing return statements
"noFallthroughCasesInSwitch": true, // Error on fallthrough cases
"noUncheckedIndexedAccess": true, // Add undefined to index signatures
"exactOptionalPropertyTypes": true, // Treat optional properties strictly
// Additional helpful settings
"noUnusedLocals": true, // Error on unused local variables
"noUnusedParameters": true, // Error on unused parameters
"noImplicitOverride": true, // Require explicit override keyword
}
}
Error Handling with Result Types
Implement robust error handling using TypeScript’s type system.
"text-[#66d9ef]">class="text-[#75715e] italic">// Result "text-[#66d9ef]">type "text-[#66d9ef]">for error handling
"text-[#66d9ef]">type Result<T, E = Error> =
| { success: "text-[#ae81ff]">true; data: T }
| { success: "text-[#ae81ff]">false; error: E };
"text-[#66d9ef]">class="text-[#75715e] italic">// Utility functions
"text-[#66d9ef]">function ok<T>(data: T): Result<T, never> {
"text-[#66d9ef]">return { success: "text-[#ae81ff]">true, data };
}
"text-[#66d9ef]">function err<E>(error: E): ResultE> {
"text-[#66d9ef]">return { success: "text-[#ae81ff]">false, error };
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Example usage
"text-[#66d9ef]">async "text-[#66d9ef]">function fetchUser(id: number): Promise<Result<User, string>> {
"text-[#66d9ef]">try {
"text-[#66d9ef]">const response = "text-[#66d9ef]">await fetch(`/api/users/${id}`);
"text-[#66d9ef]">if (!response.ok) {
"text-[#66d9ef]">return err(`HTTP error: ${response.status}`);
}
"text-[#66d9ef]">const userData = "text-[#66d9ef]">await response.json();
"text-[#66d9ef]">return ok(userData);
} "text-[#66d9ef]">catch (error) {
"text-[#66d9ef]">return err(`Network error: ${error.message}`);
}
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Usage with proper error handling
"text-[#66d9ef]">async "text-[#66d9ef]">function handleUserFetch(id: number) {
"text-[#66d9ef]">const result = "text-[#66d9ef]">await fetchUser(id);
"text-[#66d9ef]">if (result.success) {
console.log('User data:', result.data);
"text-[#66d9ef]">class="text-[#75715e] italic">// TypeScript knows result.data is User
} "text-[#66d9ef]">else {
console.error('Failed to fetch user:', result.error);
"text-[#66d9ef]">class="text-[#75715e] italic">// TypeScript knows result.error is string
}
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Chain operations safely
"text-[#66d9ef]">function processResult<T, U, E>(
result: Result<T, E>,
fn: (data: T) => Result<U, E>
): Result<U, E> {
"text-[#66d9ef]">if (result.success) {
"text-[#66d9ef]">return fn(result.data);
} "text-[#66d9ef]">else {
"text-[#66d9ef]">return result;
}
}
Advanced Interface Patterns
Create flexible and type-safe interfaces for complex scenarios.
"text-[#66d9ef]">class="text-[#75715e] italic">// Builder pattern with TypeScript
"text-[#66d9ef]">interface UserBuilder {
setName(name: string): UserBuilder;
setEmail(email: string): UserBuilder;
setAge(age: number): UserBuilder;
build(): User;
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Fluent "text-[#66d9ef]">interface with method chaining
"text-[#66d9ef]">class UserBuilderImpl "text-[#66d9ef]">implements UserBuilder {
private user: Partial<User> = {};
setName(name: string): UserBuilder {
this.user.name = name;
"text-[#66d9ef]">return this;
}
setEmail(email: string): UserBuilder {
this.user.email = email;
"text-[#66d9ef]">return this;
}
setAge(age: number): UserBuilder {
this.user.age = age;
"text-[#66d9ef]">return this;
}
build(): User {
"text-[#66d9ef]">if (!this.user.name || !this.user.email) {
"text-[#66d9ef]">throw "text-[#66d9ef]">new Error('Name and email are required');
}
"text-[#66d9ef]">return this.user as User;
}
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Factory pattern with generics
"text-[#66d9ef]">interface EntityFactory<T> {
create(data: Omit<T, 'id' | 'createdAt'>): T;
}
"text-[#66d9ef]">class UserFactory "text-[#66d9ef]">implements EntityFactory<User> {
private nextId = 1;
create(data: Omit<User, 'id' | 'createdAt'>): User {
"text-[#66d9ef]">return {
...data,
id: this.nextId++,
createdAt: "text-[#66d9ef]">new Date(),
};
}
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Observer pattern with strict typing
"text-[#66d9ef]">interface Observer<T> {
update(data: T): void;
}
"text-[#66d9ef]">interface Subject<T> {
attach(observer: Observer<T>): void;
detach(observer: Observer<T>): void;
notify(data: T): void;
}
"text-[#66d9ef]">class EventEmitter<T> "text-[#66d9ef]">implements Subject<T> {
private observers: Observer<T>[] = [];
attach(observer: Observer<T>): void {
this.observers.push(observer);
}
detach(observer: Observer<T>): void {
"text-[#66d9ef]">const index = this.observers.indexOf(observer);
"text-[#66d9ef]">if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(data: T): void {
this.observers.forEach(observer => observer.update(data));
}
}
Performance and Bundle Size Tips
Optimize your TypeScript usage for better performance and smaller bundles.
"text-[#66d9ef]">class="text-[#75715e] italic">// Use "text-[#66d9ef]">type imports to avoid runtime imports
"text-[#66d9ef]">import "text-[#66d9ef]">type { User } "text-[#66d9ef]">from './types';
"text-[#66d9ef]">import "text-[#66d9ef]">type { ComponentProps } "text-[#66d9ef]">from 'react';
"text-[#66d9ef]">class="text-[#75715e] italic">// Instead of importing the entire module
"text-[#66d9ef]">class="text-[#75715e] italic">// "text-[#66d9ef]">import { User } "text-[#66d9ef]">from './user-module'; // Might include runtime code
"text-[#66d9ef]">class="text-[#75715e] italic">// Prefer interfaces over types "text-[#66d9ef]">for object shapes (faster compilation)
"text-[#66d9ef]">interface UserProps {
name: string;
email: string;
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Use "text-[#66d9ef]">const assertions "text-[#66d9ef]">for better inference
"text-[#66d9ef]">const themes = ['light', 'dark', 'auto'] as "text-[#66d9ef]">const;
"text-[#66d9ef]">type Theme = typeof themes[number]; "text-[#66d9ef]">class="text-[#75715e] italic">// 'light' | 'dark' | 'auto'
"text-[#66d9ef]">class="text-[#75715e] italic">// Avoid deeply nested conditional types (can slow compilation)
"text-[#66d9ef]">class="text-[#75715e] italic">// ❌ Slow
"text-[#66d9ef]">type DeepConditional<T> = T "text-[#66d9ef]">extends string
? T "text-[#66d9ef]">extends `prefix-${infer Rest}`
? Rest "text-[#66d9ef]">extends `${infer First}-${infer Second}`
? [First, Second]
: never
: never
: never;
"text-[#66d9ef]">class="text-[#75715e] italic">// ✅ Better - break into smaller types
"text-[#66d9ef]">type ExtractPrefix<T> = T "text-[#66d9ef]">extends `prefix-${infer Rest}` ? Rest : never;
"text-[#66d9ef]">type SplitString<T> = T "text-[#66d9ef]">extends `${infer First}-${infer Second}`
? [First, Second]
: never;
"text-[#66d9ef]">type ProcessString<T> = T "text-[#66d9ef]">extends string
? SplitString<ExtractPrefix<T>>
: never;
Conclusion
These advanced TypeScript techniques provide powerful tools for building type-safe, maintainable applications:
- Utility types help transform existing types efficiently
- Type guards provide runtime type safety
- Generic constraints create flexible yet safe APIs
- Template literal types enable sophisticated string manipulation
- Strict configuration catches more errors at compile time
- Result types provide robust error handling
- Advanced patterns support complex architectural needs
Remember that TypeScript is not just about adding types to JavaScript – it’s about creating a better development experience through improved tooling, error detection, and code maintainability. Start with the basics and gradually incorporate these advanced techniques as your codebase grows in complexity.
The key is to find the right balance between type safety and development velocity. Use these techniques judiciously, and always prioritize code readability and maintainability over clever type gymnastics.