Skip to content

Type Inference

Rule

Let TypeScript infer your type

  • Reduced redundancy - Type inference eliminates unnecessary type annotations, making code cleaner and less repetitive.
  • Error prevention - When types are inferred, refactoring is safer because changing a variable's initialization automatically updates its type everywhere.
  • Better readability - Code becomes more concise and easier to read when not cluttered with obvious type annotations.
  • Future-proofing - Letting TypeScript infer types from libraries means your code adapts automatically when those libraries improve their type definitions.
  • Maintainability - Less repeated type information means fewer places to update when types change.

Examples

🚨 DON’T

// Redundant primitive type annotations
const loading: boolean = false;

// Obvious object type annotations
const user: { id: number; name: string } = {
    id: 1,
    name: 'John Doe'
};

// Redundant service injection types
#router: Router = inject(Router);

// Over-annotated function parameters in callbacks
this.users$ = this.http.get<User[]>('/api/users');
this.users$.subscribe((users: User[]) => {
    this.users = users;
});

// Explicit return types that match inference
const getFullName = (first: string, last: string): string => {
    return `${first} ${last}`;
}

// Explicit types for computed properties
const isValid: boolean = user.email.includes('@');

✅ DO

// Clean primitive inferences
const loading = false;

// Simple object inference
const user = {
    id: 1,
    name: "John Doe",
};

// Clean service injection
#router = inject(Router);

// Streamlined Observable subscriptions
this.users$ = this.http.get<User[]>("/api/users");
this.users$.subscribe((users) => {
    this.userList = users;
});

// Inferred return types
const getFullName = (first: string, last: string) => {
    return `${first} ${last}`;
};

// Inferred computed properties
const isValid = user.email.includes("@");

Exceptions

// Function parameters (required)
const processUser = (user: User) => {
    return user.name;
};

// Empty arrays that will be populated later
const users = [] as User[];

// Variables declared without initialization
let currentUser: User | null;

// When you need to widen or narrow a type
const status = "loading" as const; // Literal type instead of string

// Public API boundaries
export const calculateTotal = (items: CartItem[]): number => {
    return items.reduce((sum, item) => sum + item.price, 0);
};

// Complex generic constraints
const merge = <T extends Record<string, unknown>>(
    obj1: T,
    obj2: Partial<T>,
): T => {
    return {...obj1, ...obj2};
};

// When inference would be too broad
const config: AppConfig = {
    apiUrl: process.env.API_URL || "http://localhost:3000",
};