TypeScript has become the de facto standard for building large-scale JavaScript applications. Its static typing catches errors early and makes codebases more maintainable. However, to get the most out of TypeScript, you need to follow best practices.
Type Safety
Avoid any
The any type defeats the purpose of using TypeScript. Instead, use more specific types:
// Bad
function process(data: any) {
return data.value;
}
// Good
interface Data {
value: string;
}
function process(data: Data) {
return data.value;
}
Use unknown for Uncertain Types
When you truly don't know the type, use unknown instead of any:
function parseJSON(input: string): unknown {
return JSON.parse(input);
}
const result = parseJSON('{"name": "John"}');
if (typeof result === 'object' && result !== null && 'name' in result) {
console.log(result.name);
}
Interfaces vs Types
Both interfaces and types can define object shapes, but they have differences:
// Interface - can be extended and merged
interface User {
name: string;
email: string;
}
interface Admin extends User {
role: 'admin';
}
// Type - more flexible for unions and primitives
type Status = 'pending' | 'approved' | 'rejected';
type ApiResponse = { success: true; data: User } | { success: false; error: string };
Rule of thumb: Use interfaces for object shapes and types for unions, primitives, and complex type operations.
Generics
Generics make your code reusable while maintaining type safety:
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const numbers = [1, 2, 3];
const first = getFirstElement(numbers); // type: number | undefined
const strings = ['a', 'b', 'c'];
const firstString = getFirstElement(strings); // type: string | undefined
Utility Types
TypeScript provides powerful utility types:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Partial - makes all properties optional
type PartialUser = Partial<User>;
// Pick - select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// Omit - exclude specific properties
type UserWithoutPassword = Omit<User, 'password'>;
// Readonly - makes all properties readonly
type ImmutableUser = Readonly<User>;
Strict Mode
Always enable strict mode in your tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
Type Guards
Use type guards to narrow types safely:
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function processValue(value: string | number) {
if (isString(value)) {
console.log(value.toUpperCase());
} else {
console.log(value.toFixed(2));
}
}
Conclusion
TypeScript is a powerful tool, but its benefits only shine when used correctly. By following these best practices, you'll write code that's more maintainable, less error-prone, and easier for your team to work with.
Remember: TypeScript is there to help you, not hinder you. Embrace its type system, and you'll never want to go back to plain JavaScript!