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.

Go further
Live cohort on Maven

AI Transformation for Organisational Leaders

For directors, heads of function and founders: eight live evening sessions with Ehsan Gazar and Anoosh Zeighami, ending with a one-page 2027 AI plan for your organisation, built on a real workflow and ready for your leadership team.

$1,495, or $1,045 with code EARLYBIRD until 23 Oct.

View the live cohort

Keep reading