Fundamental principles that guide effective TypeScript development and type safety.
TypeScript's primary benefit is catching errors at compile time. Always prefer explicit typing over implicit any.
any type
unknown instead of
any when type is truly unknown
// ✅ Explicit types prevent runtime errors
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
function createUser(userData: Omit<User, 'id'>): User {
return {
id: Math.floor(Math.random() * 1000),
...userData
};
}
function getUserById(users: User[], id: number): User | undefined {
return users.find(user => user.id === id);
}
// ✅ Using unknown for safer type handling
function processApiResponse(data: unknown): User[] {
if (Array.isArray(data) && data.every(isUser)) {
return data;
}
throw new Error('Invalid user data format');
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as User).id === 'number' &&
typeof (value as User).name === 'string' &&
typeof (value as User).email === 'string' &&
typeof (value as User).isActive === 'boolean'
);
}
// ❌ Avoid any - loses all type safety
function badFunction(data: any): any {
return data.someProperty.thatMayNotExist; // Runtime error waiting to happen
}
// ❌ Implicit any (when noImplicitAny is false)
function calculateTotal(items) { // Parameter implicitly has 'any' type
return items.reduce((sum, item) => sum + item.price, 0);
} Choose interfaces for object shapes that might be extended, and type aliases for unions, primitives, and computed types.
// ✅ Use interfaces for object shapes and extensibility
interface BaseEntity {
id: string;
createdAt: Date;
updatedAt: Date;
}
interface User extends BaseEntity {
name: string;
email: string;
role: UserRole;
}
interface AdminUser extends User {
permissions: Permission[];
lastLogin?: Date;
}
// ✅ Use type aliases for unions and computed types
type UserRole = 'admin' | 'user' | 'guest';
type Permission = 'read' | 'write' | 'delete';
type ApiResponse<T> = {
data: T;
status: 'success' | 'error';
message?: string;
};
type UserResponse = ApiResponse<User>;
type UsersResponse = ApiResponse<User[]>;
// ✅ Use type aliases for conditional types
type NonNullable<T> = T extends null | undefined ? never : T;
type UserEmail = NonNullable<User['email']>;
// ✅ Interface declaration merging (useful for extending third-party types)
interface Window {
customProperty: string;
}
declare global {
interface Window {
gtag: (command: string, ...args: any[]) => void;
}
}
// ❌ Don't use interfaces for primitive unions
interface Status { // Should be a type alias
status: 'loading' | 'success' | 'error';
}
// ✅ Better as type alias
type Status = 'loading' | 'success' | 'error'; Handle null and undefined values explicitly to prevent runtime errors.
// ✅ Explicit null handling
interface UserProfile {
id: string;
name: string;
email: string;
avatar?: string; // Optional property
lastLogin: Date | null; // Explicit null
}
function formatUserDisplay(user: UserProfile): string {
const avatar = user.avatar ?? '/default-avatar.png';
const lastLoginText = user.lastLogin
? `Last login: ${user.lastLogin.toLocaleDateString()}`
: 'Never logged in';
return `${user.name} (${lastLoginText})`;
}
// ✅ Type guards for null checking
function isValidEmail(email: string | null | undefined): email is string {
return email != null && email.includes('@');
}
function processUser(user: UserProfile): void {
if (isValidEmail(user.email)) {
sendEmail(user.email); // TypeScript knows email is string here
}
}
// ✅ Optional chaining and nullish coalescing
function getUserDisplayName(user: UserProfile | null): string {
return user?.name ?? 'Anonymous User';
}
function getNestedProperty(obj: any): string {
return obj?.data?.user?.profile?.displayName ?? 'N/A';
}
// ✅ Strict null checks prevent common errors
function calculateLength(items: string[] | null): number {
if (items === null) {
return 0;
}
return items.length; // TypeScript knows items is not null here
}
// ❌ Avoid non-null assertion unless absolutely certain
function riskyFunction(user: UserProfile): string {
return user.avatar!.toUpperCase(); // Could throw if avatar is undefined
}
// ✅ Better - handle the null case
function safeFunction(user: UserProfile): string {
if (!user.avatar) {
throw new Error('User avatar is required');
}
return user.avatar.toUpperCase();
} Use type guards to safely narrow types and avoid unsafe type assertions.
// ✅ User-defined type guards
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isNumber(value: unknown): value is number {
return typeof value === 'number' && !isNaN(value);
}
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj
);
}
// ✅ Using type guards
function processUserData(data: unknown): User {
if (isUser(data)) {
return data; // TypeScript knows this is User
}
throw new Error('Invalid user data');
}
// ✅ Discriminated unions with type guards
interface LoadingState {
status: 'loading';
}
interface SuccessState {
status: 'success';
data: User[];
}
interface ErrorState {
status: 'error';
message: string;
}
type AppState = LoadingState | SuccessState | ErrorState;
function handleState(state: AppState): string {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'success':
return `Loaded ${state.data.length} users`; // TypeScript knows about data
case 'error':
return `Error: ${state.message}`; // TypeScript knows about message
default:
// TypeScript will error if we don't handle all cases
const exhaustiveCheck: never = state;
return exhaustiveCheck;
}
}
// ✅ Safe type assertion with validation
function assertIsUser(obj: unknown): asserts obj is User {
if (!isUser(obj)) {
throw new Error('Expected User object');
}
}
function processApiUser(data: unknown): void {
assertIsUser(data);
// data is now typed as User
console.log(data.name);
}
// ❌ Dangerous type assertions without validation
function dangerousAssertion(data: unknown): User {
return data as User; // No runtime validation - could crash
}
// ❌ Overusing type assertions
function badExample(element: HTMLElement): void {
const input = element as HTMLInputElement; // What if it's not an input?
input.value = 'test'; // Could throw at runtime
}
// ✅ Better - check the element type
function goodExample(element: HTMLElement): void {
if (element instanceof HTMLInputElement) {
element.value = 'test'; // Safe - TypeScript knows it's an input
}
} as when you're
certain about the type
any and as any casting