Type Inference and Type Compatibility in TypeScript
I used to annotate every single variable in TypeScript. Every let, every parameter, every return type. The code looked like a type dictionary.
20 Dec 2024
I used to annotate every single variable in TypeScript. Every let, every parameter, every return type. The code looked like a type dictionary.
Then I learned to trust the compiler.
Type inference
TypeScript figures out types without you spelling them out. You assign a string, it knows it's a string. You return a number from a function, it knows the return type is number.
let message = "Hello, TypeScript!"; // inferred as string
message = 42; // Error: Type 'number' is not assignable to type 'string'.
Functions work the same way:
function add(a: number, b: number) {
return a + b; // return type inferred as number
}
const result = add(5, 10); // result is number
You didn't annotate the return type. You didn't need to. TypeScript sees number + number and figures it out.
Type compatibility
TypeScript uses structural typing. It doesn't care what you named the type. It cares about the shape.
If type A has all the properties type B requires, A is compatible with B. Even if they have completely different names.
type Point = {
x: number;
y: number;
};
type NamedPoint = {
x: number;
y: number;
name: string;
};
const namedPoint: NamedPoint = { x: 15, y: 25, name: "Origin" };
const point: Point = namedPoint; // works — NamedPoint has everything Point needs
NamedPoint has an extra name property. TypeScript doesn't care. It has x and y, so it satisfies Point.
Functions follow the same principle:
type Log = (message: string) => void;
function logToConsole(msg: string) {
console.log(msg);
}
let logger: Log = logToConsole; // compatible — same shape
Inference + compatibility together
When these two features combine, you write less code and the compiler still catches your mistakes:
const points: Point[] = [
{ x: 1, y: 2 },
{ x: 3, y: 4 },
];
function printPoints(points: Point[]) {
points.forEach((point) => console.log(`(${point.x}, ${point.y})`));
}
printPoints(points); // inferred type matches the parameter type
The trade-off
Inference saves keystrokes and reduces noise. But lean too hard on it and your code becomes harder to read. A function with five parameters and no return type annotation forces the reader to trace through the implementation.
My rule: let TypeScript infer local variables and simple returns. Annotate public API boundaries — function signatures, exported types, module contracts. That's where explicit types pay for themselves.
Keep reading
- The Frontend Toolchain Is Now Written in Rust and Go. What That Means for You
- oxlint vs ESLint: Why I Replaced ESLint with oxlint
- SystemJS Is Dead. Native ESM Finally Won.
- Replace Axios with a Simple Custom Fetch Wrapper (Production-Ready Guide)
- Stop Shipping ChatGPT Wrappers. Ship an Agent in TypeScript, or Don't Bother.
- What Developers Are Saying About React 19: Pros and Cons