Chapter 29: TypeScript Type Inference
TypeScript Type Inference very slowly and clearly, like we’re sitting together with a notebook, writing small examples one by one, and really understanding when and why TypeScript decides the type for us (and when it doesn’t).
Type inference is the single most important feature that makes TypeScript feel pleasant to use instead of annoying. Without good inference → you would have to write : string, : number, : User everywhere → very verbose. With strong inference → you write types only where they add real value → the rest is figured out automatically.
1. The most important sentence you should remember today
TypeScript tries very hard to understand the type from context — but it only infers when the information is available right at the declaration.
Once a variable has a type → that type is locked (unless you use any or explicit widening tricks).
2. Four main places where inference happens (with realistic examples)
A. Variable declarations with initializer (most common)
|
0 1 2 3 4 5 6 7 8 9 10 11 |
let name = "Sara"; // inferred → string let age = 28; // inferred → number let isActive = true; // inferred → boolean let scores = [92, 85, 78]; // inferred → number[] let user = { name: "Rahul", age: 30 }; // inferred → { name: string; age: number } |
Important rule 2026: If you initialize with a literal → TypeScript infers the widest possible type (string, number, boolean, object shape), not a literal type.
|
0 1 2 3 4 5 6 |
let status = "active"; // string ← not "active" |
How to force literal type (very common pattern):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let status = "active" as const; // "active" (literal) let direction = "up" as const; // "up" let config = { theme: "dark" as const, fontSize: 16 as const, } as const; // config.theme → "dark" (literal) // config.fontSize → 16 (literal) |
B. Function return type inference
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function add(a: number, b: number) { return a + b; // inferred return type → number } function greet(name: string) { return `Hello ${name}`; // inferred → string } async function fetchUser(id: number) { // ... fetch logic ... return { id, name: "Priya" }; // inferred → Promise<{ id: number; name: string }> } |
Very useful in React hooks / utilities:
|
0 1 2 3 4 5 6 7 8 9 10 |
function useCounter(initial = 0) { const [count, setCount] = useState(initial); return { count, increment: () => setCount(c => c + 1) }; // inferred return type → { count: number; increment: () => void } } |
C. Array / object inference + contextual typing
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
const colors = ["red", "green", "blue"]; // inferred → string[] const users = [ { id: 1, name: "Aman" }, { id: 2, name: "Neha" }, ]; // inferred → { id: number; name: string }[] |
Contextual typing (TypeScript looks at where the value is used):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
interface ButtonProps { variant: "primary" | "secondary" | "danger"; } function MyButton(props: ButtonProps) { // ... } <MyButton variant="primary" />; // ok // <MyButton variant="ghost" /> // error — "ghost" not allowed |
The literal “primary” is checked against the expected union → very powerful.
D. Best common type algorithm (when multiple possibilities)
|
0 1 2 3 4 5 6 7 8 9 10 |
let value = Math.random() > 0.5 ? "hello" : 42; // inferred → string | number ← best common type let mixed = [1, "two", 3]; // inferred → (number | string)[] |
3. When inference fails or becomes too wide (common pain points)
Case 1: No initializer → no inference
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
let count; // inferred → any ← dangerous! count = 5; count = "five"; // no error — any is bad // Fix let count: number; // explicit type // or let count = 0; // initialize → number |
Case 2: Functions without parameter types
|
0 1 2 3 4 5 6 7 8 9 10 |
function add(a, b) { // parameters → any return a + b; } add(5, "10"); // no error — returns "510" |
Fix — always annotate function parameters in public APIs
Case 3: Object literals lose literal types unless as const
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const settings = { apiUrl: "https://api.example.com", timeout: 5000, }; // settings.apiUrl = "new-url"; // allowed — string // settings.timeout = "oops"; // allowed — widened to number const settingsConst = { apiUrl: "https://api.example.com", timeout: 5000, } as const; // settingsConst.apiUrl = "..."; // error — readonly + literal // settingsConst.timeout = 10000; // error |
4. Inference + generics (very powerful combo)
|
0 1 2 3 4 5 6 7 8 9 10 11 |
function identity<T>(value: T): T { return value; } const num = identity(42); // T inferred → number const str = identity("hello"); // T inferred → string |
Very common in React / hooks:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
function useLocalStorage<T>(key: string, initial: T) { // ... return [stored, setStored] as const; } const [theme, setTheme] = useLocalStorage("theme", "light"); // theme → "light" (literal if as const used inside hook) |
5. Quick cheat-sheet — inference behavior summary
| Situation | Inferred type | How to get narrower / literal type |
|---|---|---|
| let x = “hello” | string | as const → “hello” |
| let x = 42 | number | as const → 42 |
| let arr = [“a”, “b”] | string[] | as const → readonly [“a”, “b”] |
| Object literal | { prop: widened type } | as const → literal shape |
| Function return | Best common type of all return paths | Usually good — annotate if needed |
| No initializer | any | Always give explicit type |
| Generic function call | T inferred from argument | Very strong inference |
| Contextual typing (React props etc.) | Expected type from context | Very powerful — reduces annotations |
Your mini homework (try in playground right now)
- Create an object literal without as const → try changing a string property to number → see it allows
- Add as const → try the same change → see error
- Write a function that returns different types based on input → see what TS infers
- Create an array of mixed types → hover to see union array
- Try const directions = [“up”, “down”, “left”, “right”] as const → see tuple of literals
Which part feels most surprising or most useful for your current code?
Want to go deeper into:
- as const vs explicit literal unions
- Inference in React components / hooks
- When inference is too wide → how to tighten safely
- Inference with generics + constraints
Just tell me — we continue from exactly where you want! 😄
