The Primitive Types
TypeScript's primitives are JavaScript's primitives, with lower-case names: string, number, boolean, bigint, symbol, null and undefined. There is nothing new to learn about how they behave — they are the same values you already use — only new names to write down.
One point surprises students coming from Java or C. There is a single number type and it covers everything: integers, decimals, negatives, NaN and Infinity. There is no int, no float, no double. That is JavaScript's design, not a simplification TypeScript made, and it means TypeScript cannot stop you storing 3.7 where you meant a whole number. If a value must be an integer, that is a runtime check you write yourself.
bigint is for whole numbers too large for number to hold exactly, written with an n suffix. You will meet it rarely. symbol is for unique property keys and you will meet it rarer still. Do not spend time on either now; know they exist so you are not startled.
let name: string = "Ananya";
let marks: number = 87.5;
let passed: boolean = true;
// One number type for everything
let whole: number = 42;
let decimal: number = 3.14;
let notANumber: number = NaN; // still a number, as far as types go
// Rarely needed, but they exist
let big: bigint = 9007199254740993n;
let key: symbol = Symbol("id");
// Arrays of primitives
let subjects: string[] = ["Physics", "Chemistry", "Maths"]; - Because
NaNhas typenumber, an arithmetic bug that producesNaNflows through your whole program without a single type error. Types describe categories of value, not valid values within a category.
null, undefined, and the Main Reason to Use TypeScript
If TypeScript only ever did one thing for you, it would be this. Cannot read properties of undefined is the most common runtime error in JavaScript by a wide margin, and strictNullChecks — which is on whenever strict is on — is the check that prevents it.
With it on, null and undefined are not members of every type. A variable of type string holds a string and nothing else. If a value might be missing you have to say so, by writing string | null or string | undefined, and then TypeScript will not let you use that value until you have handled the missing case. The error is not the compiler being awkward; it is pointing at the exact line that would have crashed.
The three tools for handling it are worth learning together. A plain if check narrows the type for the rest of the block. Optional chaining ?. stops evaluating and produces undefined instead of throwing. The nullish coalescing operator ?? supplies a fallback, and — this matters — it only kicks in for null and undefined, unlike ||, which also replaces 0 and the empty string. Using || to default a numeric setting is a classic bug: a user who genuinely sets the value to 0 gets your default instead.
function greet(name: string | null): string {
// return name.toUpperCase();
// ERROR: 'name' is possibly 'null'.
if (name === null) return "Hello, guest";
return name.toUpperCase(); // narrowed to string here
}
interface Profile {
name: string;
address?: { city: string }; // optional -> { city: string } | undefined
}
function cityOf(p: Profile): string {
return p.address?.city ?? "Not provided";
}
// ?? vs || — a real bug, not a style preference
function pageSize(input: number | undefined): number {
return input ?? 20; // 0 stays 0
// return input || 20; // 0 silently becomes 20
} - Without
strictNullChecks,nullandundefinedare assignable to every type, and none of these errors appear. A TypeScript project with that flag off gets a fraction of the value and a full share of the extra work.
any: the Off Switch
any means "stop checking this". A value typed any can be assigned to anything, can receive anything, and you may call any method on it — including methods that do not exist. Every guarantee you set the project up for disappears for that value.
The dangerous part is that it spreads. Read a property of an any and the result is any. Pass it into a function and it satisfies any parameter. Return it and the caller's variable becomes any too. One any at the top of a data-loading function can leave an entire feature effectively untyped while your editor keeps showing you a green, error-free file.
This is why noImplicitAny matters: without it, every unannotated parameter becomes an invisible any that you never chose. There are legitimate uses — migrating a large JavaScript codebase gradually, or working around a badly typed library for an afternoon — but treat each one as a deliberate decision, mark it with a comment, and come back to it.
let value: any = "hello";
value = 42; // fine
value = { a: 1 }; // fine
value.toUpperCase(); // compiles, then throws at runtime
value.doesNotExist(); // compiles, then throws at runtime
value.a.b.c.d; // compiles. Nothing is checked at all.
// And it spreads outwards
function loadConfig(): any {
return JSON.parse("{}");
}
const config = loadConfig(); // any
const timeout = config.timeout; // any
const ms = timeout * 1000; // any — no error even if timeout is a string - Search a codebase for
: anyand you will usually find the same three places: JSON parsing, third-party libraries without types, andcatchblocks. All three have better answers, and the next section is the first of them.
unknown: What You Should Use Instead
unknown is the honest version of any. It means the same thing in English — "I do not know what this is" — but it behaves in the opposite way. Anything can be assigned into an unknown, and nothing can be done with it until you prove what it is.
That proof is a normal JavaScript check: typeof, Array.isArray, instanceof, or an in test for a property. Once TypeScript sees the check, it narrows the type inside that block and everything works normally. This is exactly the discipline you want at the edges of your program, where data arrives from outside and you genuinely do not know what it is.
So the rule is: use unknown for values whose type you have not yet established, and any only when you have decided to give up on checking. In modern TypeScript this also applies to error handling — under strict, the variable in a catch block is unknown, because JavaScript allows any value to be thrown, not just an Error. Check with instanceof Error before reading .message.
const rawText = '"hello"';
const input: unknown = JSON.parse(rawText);
// input.toUpperCase();
// ERROR: 'input' is of type 'unknown'.
if (typeof input === "string") {
console.log(input.toUpperCase()); // narrowed to string — fine
}
// A safe boundary function
function describe(value: unknown): string {
if (typeof value === "number") return `number: ${value.toFixed(2)}`;
if (typeof value === "string") return `string of length ${value.length}`;
if (Array.isArray(value)) return `array of ${value.length}`;
return "something else";
}
// catch is unknown under strict — not Error
function risky(): void {
throw new Error("network down");
}
try {
risky();
} catch (err) {
if (err instanceof Error) console.error(err.message);
else console.error("Unknown failure", err);
} - A neat way to remember the difference:
anysays "trust me",unknownsays "prove it". Only one of those is still true six months later.
void, never, and Literal Types
void is the return type of a function that does not return anything useful. It is not the same as undefined: a void return means "do not use my return value", which is a statement about intent, whereas undefined is an actual value someone might read.
never is the type of a value that cannot exist. A function typed never does not return at all — it throws, or it loops forever. You will also see never appear on its own when TypeScript has narrowed a union down to nothing left, and that turns out to be extremely useful: it is how you make the compiler tell you that a switch has stopped covering every case. The lesson on type guards uses this trick properly.
Literal types are the quiet star of this lesson. A type can be one exact value — not just string but the string "pending". On their own they are useless; combined with a union they replace loose strings with a fixed set of valid options that your editor can autocomplete and the compiler can enforce. This is how you model an order status, a button variant, a user role, or an HTTP method, and it is one of the highest-value patterns in everyday TypeScript.
// void: the return value is not meant to be used
function logEvent(name: string): void {
console.log(name);
}
// never: this function does not finish
function fail(message: string): never {
throw new Error(message);
}
// Literal types combined into a union
type OrderStatus = "placed" | "packed" | "shipped" | "delivered";
let status: OrderStatus = "packed"; // OK, and autocompleted by the editor
status = "shiped";
// ERROR: Type '"shiped"' is not assignable to type 'OrderStatus'.
// The same idea for numbers and booleans
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
function move(steps: DiceRoll) { /* ... */ }
move(3); // OK
move(7); // ERROR Type Assertions Are a Claim, Not a Conversion
Sometimes you know something the compiler cannot work out — usually when dealing with the DOM or with data from outside your program. A type assertion, written value as SomeType, tells TypeScript to treat the value as that type from here on.
Understand exactly what happens: nothing. There is no conversion, no check, no code emitted. as is you overruling the compiler, and if you are wrong, the compiler was your last line of defence and you just removed it. const n = "42" as unknown as number compiles cleanly and then behaves like a string at runtime, because it is a string. Assertions can lie, and they lie silently.
The most common honest use is the DOM. document.getElementById returns HTMLElement | null — correct, because the element might not be there — and HTMLElement has no value property because not every element is an input. If you wrote the HTML and know it is an input, asserting is reasonable. The safer alternative is a real check that also handles the case where the element genuinely is missing.
The related operator is the non-null assertion, a trailing !, which means "this is not null or undefined, I promise". It is the same kind of promise, with the same consequences if you are wrong. Use both sparingly and never as a way to make a red squiggle go away.
// The DOM case: an honest use of 'as'
const emailInput = document.getElementById("email") as HTMLInputElement;
emailInput.value = "student@example.com";
// Safer: check instead of asserting
const el = document.getElementById("email");
if (el instanceof HTMLInputElement) {
el.value = "student@example.com";
}
// Non-null assertion — same promise, shorter syntax
const form = document.querySelector("form")!;
// How an assertion lies. This compiles. It is wrong.
const raw: unknown = "42";
const count = raw as unknown as number;
console.log(count + 1); // "421" — it was a string all along
// 'as const' is the useful, safe assertion: freeze to literal types
const ROLES = ["admin", "teacher", "student"] as const;
type Role = typeof ROLES[number]; // "admin" | "teacher" | "student" - TypeScript blocks assertions between completely unrelated types, which is why people reach for the double assertion
as unknown as X. Treat that phrase as a warning sign in a code review: it means someone forced the compiler to accept something it had specifically rejected. - You may also see the older angle-bracket form,
<string>value. It means the same thing but is invalid in.tsxfiles, where it clashes with JSX. Useaseverywhere.
