Paulund
Subscribe

Type Annotations vs Type Inference: When to Write the Type

21 April 2026, 4 min read

From the archive. Versions and APIs may have moved on since I wrote this.

On this page
  1. What TypeScript Infers Without Help
  2. Where Inference Goes Wrong
  3. When Annotations Actually Help
  4. The Annotation Doubles as a Check
  5. The Rule I Actually Follow

When I first started with TypeScript, I added types everywhere. Every variable, every parameter, every return value. It felt like that was the point. You're opting into a typed language, so you write types, right?

After a while I swung the other way and let inference handle everything. Why repeat yourself when the compiler already knows?

Neither extreme is right. The question isn't whether TypeScript can infer the type. It usually can. The question is whether an explicit annotation adds clarity that inference alone doesn't provide.

What TypeScript Infers Without Help

TypeScript's inference is good. For most local variables, it gets the type right immediately.

TypeScript
const count = 0;        // inferred: number
const label = "hello";  // inferred: string
const active = true;    // inferred: boolean

const user = {
  id: 1,
  name: "Paul",
};
// inferred: { id: number; name: string }

Function return types are also inferred from what the function body actually returns.

TypeScript
function double(n: number) {
  return n * 2;
}
// return type inferred: number

Arrays work too.

TypeScript
const ids = [1, 2, 3];        // inferred: number[]
const mixed = [1, "two", 3];  // inferred: (string | number)[]

For short, local code this is fine. You write less and the compiler still catches mistakes.

Where Inference Goes Wrong

The problem shows up when inference is technically correct but practically misleading.

let and mutable variables

TypeScript
let status = "loading";

TypeScript infers string. That means later you could accidentally do this:

TypeScript
status = "any random string";

And TypeScript won't complain. If you intended status to only ever be one of a few values, annotation beats inference.

TypeScript
let status: "loading" | "success" | "error" = "loading";

Now the constraint is visible and enforced.

Object shapes without an explicit type

TypeScript
const config = {
  retries: 3,
  timeout: 5000,
};

TypeScript infers { retries: number; timeout: number }. That's accurate, but if config is meant to satisfy a known interface, you won't get an error if you misspell a key or add an extra property that doesn't belong.

Annotating against the interface catches that:

TypeScript
interface Config {
  retries: number;
  timeout: number;
}

const config: Config = {
  retries: 3,
  timeout: 5000,
};

Arrays initialised empty

TypeScript
const results = [];

TypeScript infers never[] here, because it has no information. Any push will be a type error. You almost always need an annotation.

TypeScript
const results: string[] = [];

When Annotations Actually Help

There are a few situations where I always write an explicit annotation, regardless of what inference would do.

Function parameters. Inference cannot reach backwards from how a function is called to figure out parameter types. You must annotate them.

TypeScript
function greet(name: string): string {
  return `Hello, ${name}`;
}

Public API boundaries. Exported functions, class methods, module-level constants. If the code is a surface others consume, annotations document intent and ensure the public type is what you meant, not just what you happened to write.

TypeScript
export function parseId(raw: unknown): number {
  if (typeof raw !== "number") throw new Error("not a number");
  return raw;
}

When the inferred type is too broad. Inference widens literal values to their base types.

TypeScript
const direction = "left"; // inferred: string, not "left"

If you need the literal, either annotate or use as const.

TypeScript
const direction = "left" as const;  // inferred: "left"

Complex generics. When a generic function's return type is hard to follow from the implementation, an explicit return annotation is a gift to the next reader.

TypeScript
function groupBy<T, K extends string>(
  items: T[],
  key: (item: T) => K
): Record<K, T[]> {
  // ...
}

The Annotation Doubles as a Check

One thing I underappreciated early on: annotating the return type of a function makes TypeScript verify that your implementation matches your intent, not the other way around.

TypeScript
function getUser(): User {
  return {
    id: 1,
    // forgot name — TypeScript catches this
  };
}

Without the return annotation, TypeScript infers the type from what you returned, so a partial object just becomes the inferred type. With the annotation, you get an error. The annotation is a constraint, not just a label.

The Rule I Actually Follow

Inference: use it for local variables where the type is obvious from the right-hand side.

Annotations: use them at boundaries. Function parameters always. Return types when the function is exported or complex. Variables where the inferred type is too wide for your intent.

A rough check: if you deleted the annotation, would a reader of that line have to trace backwards to understand the type? If yes, keep it.

If the type is obvious from context, removing the annotation makes the line quieter without losing information. Quiet code is easier to scan. But when an annotation says "this must be exactly this type, not whatever I happened to construct", it earns its place.

Next in TypeScript

27 April 2026Validating Multi-Provider Credentials with Zod Discriminated Unions

Tags

  • TypeScript
  • JavaScript
  • Frontend
  • Development