Skip to main content

Module 21: Type Inference Deep Dive

Understanding how TypeScript automatically infers types helps you write cleaner, more maintainable code with minimal annotations.


1. Basic Type Inference

// Variable inference
let name = "Alice"; // string
let age = 25; // number
let isActive = true; // boolean

// Array inference
let numbers = [1, 2, 3]; // number[]
let mixed = [1, "two", true]; // (string | number | boolean)[]

2. Best Common Type

let items = [1, 2, 3, null];  // (number | null)[]
let values = [new Date(), "2024-01-01"]; // (string | Date)[]

// With explicit supertype
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

let pets = [new Dog(), new Cat()]; // (Dog | Cat)[]
let animals: Animal[] = [new Dog(), new Cat()]; // Animal[]

3. Contextual Typing

window.addEventListener("click", (event) => {
// event is inferred as MouseEvent
console.log(event.clientX, event.clientY);
});

const numbers = [1, 2, 3];
numbers.forEach((num) => {
// num is inferred as number
console.log(num.toFixed(2));
});

4. Return Type Inference

function add(a: number, b: number) {
return a + b; // Return type inferred as number
}

function getUser() {
return { name: "Alice", age: 25 };
// Return type inferred as { name: string; age: number; }
}

async function fetchData() {
return "data"; // Return type inferred as Promise<string>
}

5. Generic Inference

function identity<T>(value: T): T {
return value;
}

let num = identity(42); // T inferred as number
let str = identity("hello"); // T inferred as string

// Multiple generics
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}

let result = pair("age", 25); // [string, number]

6. Inference from Usage

// Function parameter inference
const processUser = (user: { name: string; age: number }) => {
return user.name.toUpperCase();
};

// TypeScript infers the return type is string

7. Inference with Conditional Types

type Unpromise<T> = T extends Promise<infer U> ? U : T;

declare function unwrap<T>(value: T): Unpromise<T>;

let str = unwrap(Promise.resolve("hello")); // string
let num = unwrap(42); // number

8. Literal Type Inference

let direction = "up";  // string (widened)
const direction2 = "up"; // "up" (literal)

// Force literal type
let direction3 = "up" as const; // "up"

// Object as const
const config = {
host: "localhost",
port: 3000
} as const;
// { readonly host: "localhost"; readonly port: 3000; }

9. Inference in Generics

function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}

let numbers = [1, 2, 3];
let strings = map(numbers, n => n.toString());
// U inferred as string from return type of n.toString()

10. Type Widening

let x = null;  // any (widened)
let y = undefined; // any (widened)

const z = null; // null (not widened)

// Prevent widening
let value = "hello" as const; // "hello"

Key Takeaways

✅ TypeScript infers types from values and context
Best common type for unions
Contextual typing in callbacks
Generic inference from arguments
as const prevents type widening


Practice Exercises

Exercise 1: Infer Complex Types

function createPerson(name: string, age: number) {
return {
name,
age,
greet() {
return `Hello, I'm ${this.name}`;
}
};
}

type Person = ReturnType<typeof createPerson>;

Next Steps

In Module 22, we'll explore the TypeScript Compiler API for programmatically working with TypeScript code.