Types and inference
What you annotate, what the compiler works out for itself, and why any is a hole.
After this lesson you can
- Annotate a variable, a parameter and a return type
- Say when to annotate and when to let inference do it
- Explain the difference between any and unknown
TypeScript is JavaScript with types checked before the code runs. Nothing survives to runtime: the types are erased, and what ships is JavaScript.
const name: string = "Nino";
const years: number = 5;
const remote: boolean = true;
Those annotations are all unnecessary. TypeScript already knows "Nino" is
a string. Writing the type again adds nothing and gives you a second thing
to keep in sync.
Annotate the edges
The useful rule: annotate what crosses a boundary, infer the rest.
function fullName(first: string, last: string): string {
const joined = `${first} ${last}`; // inferred as string
return joined;
}
Parameters need annotations, because nothing tells the compiler what a
caller will pass; under strict an unannotated parameter is an error rather
than a silent any. (A callback's parameters are the exception: they are
inferred from the place the callback is passed to.) A return type is optional but worth writing on anything
exported: it makes the function's promise explicit, and it means a mistake
inside the function is reported inside the function rather than at every
call site.
Try it
export function describe(): string { // No annotations anywhere; TypeScript infers every one of these. const name = "Nino"; const years = 5; const skills = ["C#", "PostgreSQL"]; // skills is string[], so .join is available and .toFixed is not. return `${name}, ${years} years, ${skills.join(" and ")}`;}One thing to know about this editor: like most build tools (esbuild, swc,
tsx, Vite), it strips the types and runs the JavaScript without
type-checking. A type error here shows up only if the code actually goes
wrong when it runs. Type-checking is a separate step — tsc --noEmit in CI,
or your editor's squiggles — and a project that skips it has types that are
never enforced.
any and unknown
any switches the checker off for that value. Everything is allowed, and
the hole spreads: anything derived from an any is also unchecked.
unknown is the safe version. You can hold it, pass it, store it — you just
cannot use it until you have proved what it is.
function parse(raw: unknown): number {
if (typeof raw === "number") return raw; // proved
if (typeof raw === "string") return Number(raw);
throw new Error("not a number");
}
Whenever you are about to write any, unknown plus a check is almost
always what you actually wanted.
Types do not exist at runtime
Because the types are erased, a type annotation never converts or checks a value. If data arrives from outside — JSON, a form, an environment variable — the annotation is only a claim about it:
interface Profile { name: string; years: number;} export function run(): unknown[] { // JSON.parse returns any, so this assignment is accepted without a check. const profile: Profile = JSON.parse('{"name":"Nino","years":"5"}'); // The type says number. The value is the string "5", so + concatenates. const nextYear = profile.years + 1; return [typeof profile.years, nextYear];}Common mistakes
- Treating an annotation as a check.
const p: Profile = JSON.parse(...)compiles becauseJSON.parsereturnsany; the example above shows a "number" that is really the string"5", so+ 1gives"51". - Silencing an error with
any. The error goes away and so does every check on everything derived from that value; the bug moves to runtime. - Annotating what inference already knows.
const n: number = 5is noise, andconst status: string = "draft"is worse: it widens the literal"draft"tostringand throws information away. - Assuming the build type-checks. Bundlers that strip types will happily
ship code
tscwould reject. Runtsc --noEmitsomewhere that can fail the build.
What interviewers ask
- When do you write a type annotation, and when do you let inference work?
Annotate boundaries — parameters, exported return types, values whose
inferred type is too wide or too narrow (an empty
[]); infer locals.
Try it yourself
2 visible tests · 5 hidden testsImplement formatDuration(seconds: number): string, with both the
parameter and the return type annotated. seconds is a whole number,
zero or more.
- Under one hour, return
"m:ss": minutes without padding, seconds always two digits (75→"1:15",5→"0:05"). - From one hour up, return
"h:mm:ss": hours without padding, minutes and seconds always two digits (3725→"1:02:05").
formatDuration(75)formatDuration(3725)
Sign up to run the tests, check the hidden ones and save your progress. Sign up