Basic types and type inference
TypeScript can usually figure out a variable's type without being told — knowing when to rely on that and when to write the annotation yourself is the first real judgment call the language asks you to make.
5 min read
The primitive types, and how close they map to JavaScript's own
let age: number = 25;
let name: string = "Ada";
let isStudent: boolean = true;
let nothing: null = null;
let notSet: undefined = undefined;number, string, and boolean correspond directly to JavaScript's own primitive types — TypeScript doesn't invent a separate numeric type system the way some languages distinguish int from float; there's just number, matching JavaScript's single floating-point number type underneath. null and undefined are also real types here, not just values — worth remembering once the strictness lesson later covers exactly how seriously TypeScript treats the distinction between "a string" and "a string, or possibly null."
Type inference: TypeScript often already knows, without being told
let age = 25; // inferred as number, no annotation needed
let name = "Ada"; // inferred as string
age = "not a number"; // Error: Type 'string' is not assignable to type 'number'Writing : number on age here would be redundant — TypeScript looks at the initial value, 25, and infers the variable's type from it automatically. This isn't a weaker or looser check than an explicit annotation; age is genuinely, fully typed as number from this point forward, exactly as if you'd written it out. Explicit annotations exist for the cases inference genuinely can't cover on its own — most commonly, a function's parameters (which have no initial value for TypeScript to look at).
Function parameters need explicit annotations — there's nothing to infer from
// x has no initial value TypeScript can look at — it needs to be told
function double(x: number): number {
return x * 2;
}
double("5"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'A variable declared with an initial value (let age = 25) gives TypeScript something to infer from. A function parameter has nothing like that at the point it's declared — x in function double(x) could be anything until it's annotated. Without strict mode (covered in its own lesson), an unannotated parameter silently becomes type any — effectively opting that one parameter out of type checking entirely, which defeats much of the point of using TypeScript at all.
Return types: usually inferred too, but worth writing explicitly on public functions
function double(x: number) {
return x * 2; // return type inferred as number, no ": number" needed
}
function getUser(id: number) {
if (id === 0) return null;
return { id, name: "Ada" }; // inferred return type: { id: number; name: string; } | null
}TypeScript infers a function's return type from its return statements, the same way it infers a variable's type from its initializer — double above is inferred to return number without writing : number explicitly. This works even across multiple, different return statements, producing a union of every possible returned shape (getUser's inferred return type combines both branches). Explicit return-type annotations are still worth writing on a function meant to be called from other files — they act as a locked-in contract: if a later edit accidentally changes what the function returns, that annotation turns the mismatch into an immediate compile error at the function itself, rather than a confusing error somewhere downstream at a call site.
Arrays and tuples: two genuinely different shapes
let scores: number[] = [10, 20, 30]; // an array: any length, all numbers
let point: [number, number] = [3, 4]; // a tuple: exactly 2 elements, these specific types
scores.push(40); // fine — arrays can grow
point.push(5); // technically allowed by TS (a known tuple quirk), but destroys the "exactly 2" intent
point = [3, 4, 5]; // Error: Source has 3 element(s) but target allows only 2number[] (equivalently, Array<number>) describes an array of any length where every element is a number. A tuple, [number, number], describes a fixed-length array where each position has its own specific type — the first element must be a number, the second must be a number, and there can only be exactly two. Tuples are the right tool whenever a value's meaning depends on its position, not just its element type — a coordinate pair, or a [key, value] entry from Object.entries().
any vs unknown: both mean "could be anything," with a real safety difference
let a: any = fetchSomeData();
a.whatever.deeply.nested(); // no error at all — any disables checking entirely
let u: unknown = fetchSomeData();
u.whatever(); // Error: Object is of type 'unknown'
if (typeof u === "string") {
u.toUpperCase(); // fine — narrowed to string inside this branch
}any turns off type checking for that value completely — every operation on it compiles without complaint, silently reintroducing exactly the class of bug TypeScript exists to catch. unknown also means "could be anything," but the compiler refuses to let you do anything with an unknown value until you've actually proven what it is (the narrowing lesson covers this mechanism in depth) — unknown is the genuinely type-safe way to represent "I don't know this value's type yet," and any should be treated as an escape hatch of last resort, not a convenient default.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Given `let age = 25;` with no type annotation, what does TypeScript do?
2. Why do function parameters usually need explicit type annotations, unlike a `let` variable with an initial value?
3. What is the key safety difference between `any` and `unknown`?
4. What's the actual difference between `number[]` and `[number, number]`?