Idiomatic TypeScript annotates boundaries and lets inference do the rest, which keeps code short and still safe.
You rarely need to annotate everything. TypeScript infers a type from an initializer: let count = 0 makes count a number, and const names = ["a", "b"] is string[]. Hover in your editor to see what was inferred.
Where annotations earn their keep:
- Function parameters — they can't be inferred from the body (and without
strict'snoImplicitAnythey silently becomeany). - Public return types of exported functions, so a change in the body can't silently change the API.
- Variables declared without a value (
let result: string;) and empty collections (const ids: number[] = []). - When you want a wider or narrower type than the one inferred.
Inference also flows "backwards" through contextual typing: in names.map(n => n.length) the callback parameter n is a string because map knows the array's element type.
Watch literal widening. A const binding can never change, so const mode = "dark" has the literal type "dark". A let can be reassigned, so let mode = "dark" widens to string. Object properties are mutable too, so { mode: "dark" } has mode: string. This matters when a function expects the literal type "dark" | "light" — later lessons show as const and satisfies for those cases.
A type annotation (const x: T = value) asks the checker to verify the value fits T. A type assertion (value as T) instead tells the checker to trust you and skips most checking. Prefer annotations; reach for as only when you know something the compiler can't.
Example
let count = 0; // number
const mode = "dark"; // "dark" (literal type)
let theme = "dark"; // string (let widens)
const settings = { mode: "dark" }; // { mode: string }
const lengths = ["ab", "cde"].map(s => s.length); // s: string, result: number[]
// Annotate parameters and exported return types:
export function average(values: number[]): number {
return values.reduce((a, b) => a + b, 0) / values.length;
}
const ids: number[] = []; // without the annotation this would be any[] / never[]
ids.push(count);
// Assertion = "trust me" (no conversion happens at runtime!)
const input = JSON.parse('{"n": 1}') as { n: number };
console.log(mode, theme, settings, lengths, average([1, 2]), input.n);Remember
- Annotate parameters, exported return types and empty values; let inference do the rest
constkeeps literal types;letand object properties widen them- Annotation = checked;
asassertion = trusted, not checked
Watch out for
- Using
asto silence an error rather than fixing the type — it hides real bugs.