TypeScript

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

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.

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.

Typescript
let message = "Hello, TypeScript!"; // inferred as string
message = 42; // Error: Type 'number' is not assignable to type 'string'.

Functions work the same way:

Typescript
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.

Typescript
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:

Typescript
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:

Typescript
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