📚 4 min read
Type Inference in TypeScript ​
This section explores TypeScript's type inference capabilities and best practices for leveraging them effectively.
Overview ​
Type inference is TypeScript's ability to automatically determine types based on context. Understanding how inference works helps write more concise and maintainable code.
Basic Type Inference ​
Variable Initialization ​
typescript
// Type: string
let name = 'John';
// Type: number
let age = 30;
// Type: boolean
let isActive = true;
// Type: string[]
let fruits = ['apple', 'banana', 'orange'];
// Type: { name: string, age: number }
let person = {
name: 'John',
age: 30,
};
Function Return Types ​
typescript
// Return type: number
function add(a: number, b: number) {
return a + b;
}
// Return type: string
function greet(name: string) {
return `Hello, ${name}!`;
}
// Return type: boolean
function isEven(num: number) {
return num % 2 === 0;
}
Advanced Inference ​
Generic Type Inference ​
typescript
// Type parameter T is inferred
function identity<T>(value: T): T {
return value;
}
// Type: string
const str = identity('hello');
// Type: number
const num = identity(42);
// Type: { name: string }
const obj = identity({ name: 'John' });
Array Method Inference ​
typescript
const numbers = [1, 2, 3, 4, 5];
// Type: number[]
const doubled = numbers.map((n) => n * 2);
// Type: number[]
const evenNumbers = numbers.filter((n) => n % 2 === 0);
// Type: number
const sum = numbers.reduce((acc, n) => acc + n, 0);
// Type: { value: number }[]
const objects = numbers.map((n) => ({ value: n }));
Contextual Typing ​
Event Handlers ​
typescript
const button = document.querySelector('button');
button?.addEventListener('click', (event) => {
// event is inferred as MouseEvent
console.log(event.clientX, event.clientY);
});
window.addEventListener('keydown', (event) => {
// event is inferred as KeyboardEvent
console.log(event.key, event.code);
});
Promise Callbacks ​
typescript
async function fetchUser(id: string) {
const response = await fetch(`/api/users/${id}`);
// result is inferred as any
const result = await response.json();
return result;
}
// Better type inference with generics
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
// result is inferred as T
const result = await response.json();
return result;
}
Real-World Example ​
typescript
// Domain types
interface User {
id: string;
name: string;
email: string;
}
interface Post {
id: string;
title: string;
content: string;
authorId: string;
}
// Service class with inferred types
class DataService {
private cache = new Map<string, unknown>();
// Method with inferred return type
async fetchOne<T>(resource: string, id: string) {
const cacheKey = `${resource}:${id}`;
if (this.cache.has(cacheKey)) {
// Type is inferred from Map's value type
return this.cache.get(cacheKey) as T;
}
const response = await fetch(`/api/${resource}/${id}`);
const data = await response.json();
this.cache.set(cacheKey, data);
return data as T;
}
// Method with inferred array type
async fetchMany<T>(resource: string, query = {}) {
const params = new URLSearchParams(query as Record<string, string>);
const response = await fetch(`/api/${resource}?${params}`);
const data = await response.json();
return data as T[];
}
// Method with inferred mapped type
async fetchRelated<T, R>(resource: string, id: string, relation: string) {
const response = await fetch(`/api/${resource}/${id}/${relation}`);
const data = await response.json();
return data as R[];
}
}
// Usage with type inference
async function getUserWithPosts(userId: string) {
const service = new DataService();
// user is inferred as User
const user = await service.fetchOne<User>('users', userId);
// posts is inferred as Post[]
const posts = await service.fetchRelated<User, Post>(
'users',
userId,
'posts'
);
return {
user,
posts,
// postCount is inferred as number
postCount: posts.length,
// hasPublished is inferred as boolean
hasPublished: posts.length > 0,
// latestPost is inferred as Post | undefined
latestPost: posts.sort(
(a, b) => new Date(b.id).getTime() - new Date(a.id).getTime()
)[0],
};
}
// Type inference with array methods
function processUserData(users: User[]) {
// emailMap is inferred as Map<string, User>
const emailMap = new Map(users.map((user) => [user.email, user]));
// usersByDomain is inferred as Map<string, User[]>
const usersByDomain = users.reduce((acc, user) => {
const domain = user.email.split('@')[1];
return acc.set(domain, [...(acc.get(domain) || []), user]);
}, new Map<string, User[]>());
// stats is inferred as { domain: string, count: number }[]
const stats = Array.from(usersByDomain.entries()).map(([domain, users]) => ({
domain,
count: users.length,
}));
return {
emailMap,
usersByDomain,
stats,
};
}
Best Practices ​
Type Inference Usage:
- Let TypeScript infer simple types
- Explicitly type complex structures
- Use type annotations for clarity
Generic Type Inference:
- Leverage generic type parameters
- Use constraints when needed
- Provide type hints when inference fails
Return Type Inference:
- Allow inference for simple functions
- Explicitly type complex return types
- Use type annotations for public APIs