Type Annotations
A type annotation is an explicit declaration that tells TypeScript what type a value should have, written with a colon after the name. While type inference handles most local code automatically, annotations are how you state intent at the places the compiler cannot read — function parameters, uninitialized variables, and public API boundaries. They double as living documentation and as contracts the compiler enforces on every caller. This page covers the annotation syntax for variables, functions, objects, and arrays, plus when an annotation beats inference.
Variable annotations
A variable annotation goes between the name and the optional initializer. With an initializer it is usually redundant, but it is required when you declare a binding before assigning it.
let username: string = "ada"; // redundant — inference already gives string
let total: number; // ✅ needed: no initializer to infer from
total = 19.99;
// ❌ Error: Type 'boolean' is not assignable to type 'number'.
total = true;
When a value is present, prefer letting inference do the work and reserve annotations for declarations without an initializer. An annotation also lets you intentionally choose a wider type than the initializer would infer, such as declaring a union up front.
Function parameter annotations
Function parameters are the single most important place to annotate. Outside of contextual typing, the compiler has no value to infer a parameter from, so under noImplicitAny every parameter must be typed.
function multiply(a: number, b: number): number {
return a * b;
}
// Optional and default parameters
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}`;
}
function findUser(id: number, includeDeleted?: boolean) {
// includeDeleted: boolean | undefined
}
Each parameter is annotated independently. A ? marks a parameter optional (its type becomes T | undefined), while a default value both makes it optional and lets inference determine the type from that default. Parameters need annotations precisely because there is no initializer or surrounding context to infer them from.
Return type annotations
The return type goes after the parameter list. TypeScript can infer it, but annotating it on exported functions pins the contract so an accidental change to the body becomes an error at the source rather than at every call site.
function parseAge(input: string): number {
const n = Number(input);
// ❌ Error: Type 'string' is not assignable to type 'number'.
if (Number.isNaN(n)) return "invalid";
return n;
}
const toUpper = (s: string): string => s.toUpperCase();
Because the return type is declared as number, returning "invalid" is flagged right inside parseAge — not later where the result is consumed. For internal helpers you can usually omit this and let inference carry the type.
Object and array annotation syntax
Objects are annotated with an inline type literal (or a named object type), and arrays with T[] or the equivalent Array<T>.
const point: { x: number; y: number } = { x: 0, y: 0 };
const scores: number[] = [98, 72, 85];
const names: Array<string> = ["Ada", "Grace"];
const matrix: number[][] = [[1, 2], [3, 4]];
const pair: [string, number] = ["age", 30]; // a tuple
number[] and Array<number> are identical — pick one style and stay consistent. A fixed-length, positional array is a tuple, annotated with bracketed element types. For reusable shapes, extract the inline object type into a named type or interface rather than repeating it.
Annotation vs assertion
An annotation asks the compiler to check that a value matches a type; a type assertion (as) tells the compiler to trust you and skip that check. They look similar but have opposite safety guarantees.
// Annotation: checked — the value must actually conform
const a: HTMLElement = document.createElement("div"); // ✅ verified
// Assertion: trusted — no check, your responsibility if wrong
const b = document.getElementById("app") as HTMLDivElement;
// ❌ Annotation catches the mismatch
const c: number = "5"; // Error: Type 'string' is not assignable to type 'number'.
// ⚠️ Assertion silences it — and can be wrong at runtime
const d = "5" as unknown as number; // compiles, but `d` is really a string
Prefer annotations: they verify your assumption. Reach for an assertion only when you genuinely know more than the compiler — for example narrowing a DOM lookup. See Type Assertions for the full rules and the safer satisfies operator.
Inference vs annotation trade-offs
Both approaches produce identical type safety; the difference is verbosity, intent, and where errors surface. Use this table to decide.
| Aspect | Inference | Explicit annotation |
|---|---|---|
| Verbosity | Minimal | More to write and maintain |
| Function parameters | Not possible (no source) | Required |
| Uninitialized variables | Not possible | Required |
| Local initialized variables | Ideal | Redundant noise |
| Public API contracts | Can drift | Pins a stable contract |
| Error location | At usage | At the declaration |
| Intent / documentation | Implicit | Explicit and self-documenting |
Annotating a value with a type that is wider than its initializer is a deliberate, safe widening (e.g.
let status: string). Annotating narrower than the value, however, requires the value to actually conform — and if it cannot, that is a real error worth fixing.
Best Practices
- Always annotate function parameters — they cannot be inferred and
noImplicitAnyrequires them. - Skip annotations on local variables that have an initializer; let inference work.
- Annotate return types on exported and public-facing functions to lock the contract.
- Prefer a type annotation (checked) over a type assertion (trusted) whenever possible.
- Use
T[]consistently rather than mixing it withArray<T>in the same codebase. - Extract repeated inline object annotations into a named
typeorinterface. - Annotate to intentionally widen, but never use an assertion just to silence a real error.