Notifications

No notifications

/Phase 1

Primitive & Built-in Types

Before you build interfaces, generics, or React props, you need fluency with the building blocks: string, number, boolean, null, undefined, and the three special citizens β€” any, unknown, and never. Understanding when to annotate vs let TS infer, and the difference between literal and widened types, is what separates beginner TS from professional TS.

On this page

Detailed Theory

The primitives

TypeScript's primitive types map 1-to-1 with JavaScript's:

TypeExampleNotes
string"hi", \hello ${x}\``UTF-16 strings
number42, 3.14, 0xff, Infinity, NaNAll numbers are 64-bit floats
booleantrue, false
bigint9007199254740993nArbitrary-precision integers
symbolSymbol("id")Unique runtime identifiers
nullnullIntentional absence
undefinedundefinedDefault uninitialised value

> Lowercase string/number/boolean are the primitive types. The capitalised String/Number/Boolean are the boxed wrapper objects β€” almost never what you want.

let firstName: string = "Rahul";
let age: number = 21;
let isAdmin: boolean = false;
let id: bigint = 100n;
let tag: symbol = Symbol("user");

Inference vs annotation

TypeScript infers types from initialisers. Don't annotate when inference is obvious β€” it's just visual noise.

const PI = 3.14;             // inferred: 3.14 (literal)
let counter = 0;             // inferred: number
const colours = ["red", "blue"]; // inferred: string[]

// Annotate when there's no value to infer from: let nextId: number; function area(w: number, h: number): number { return w * h; }

Literal types & widening

const infers the *literal* value. let widens to the broader type.

const greeting = "hello";   // type is exactly "hello"
let salutation = "hello";   // type is string

type Direction = "up"

"down""left"
"right"; let move: Direction = "up"; // βœ… move = "diagonal"; // ❌

That's how libraries give you autocomplete on string options β€” they're literal-union types, not free-form strings.

as const

To freeze an object/array into a deeply readonly literal:

const config = {
  endpoint: "/api",
  retries: 3,
} as const;
// config: { readonly endpoint: "/api"; readonly retries: 3 }

any, unknown, never

These three are TypeScript's "escape hatches". Knowing the difference is the most common interview question.

any β€” opt out of the type system

Anything goes. TS will not check anything you do with it. Treat it as a code smell β€” every any is a place a runtime bug can hide.

let x: any = "hi";
x.toFixed();   // βœ… no error… but crashes at runtime

unknown β€” the safe any

You can assign anything to it, but you can't *do* anything with it until you narrow it.

function parse(input: unknown) {
  // input.toFixed();  // ❌
  if (typeof input === "number") {
    return input.toFixed(2); // βœ… narrowed to number
  }
  return String(input);
}

Rule: when you receive data from outside (JSON, fetch, localStorage), type it as unknown and validate.

never β€” the impossible type

A function that never returns (throws or loops forever) returns never. The exhaustive-check pattern uses it to make the compiler force you to handle every case.

function fail(msg: string): never {
  throw new Error(msg);
}

type Shape = { kind: "circle" } | { kind: "square" }; function area(s: Shape) { switch (s.kind) { case "circle": return 1; case "square": return 2; default: const _exhaustive: never = s; // errors if you add a new shape and forget here return _exhaustive; } }

null and undefined

With strictNullChecks (part of strict), null and undefined are not assignable to other types. You must opt in:

let name: string = null;        // ❌
let nick: string 
null = null; // βœ…

function find(id: number): User

undefined { /* … */ } const u = find(1); // u.name; // ❌ Object is possibly 'undefined' u?.name; // βœ… optional chaining

Type assertions β€” as

When *you* know more than the compiler, use as. Use sparingly β€” it's a promise the compiler can't verify.

const el = document.getElementById("app") as HTMLDivElement;
const data = JSON.parse(json) as User;

Avoid the angle-bracket form el β€” it clashes with JSX.

Forbidden: double assertions

const num = "hi" as unknown as number; // 🚨 you're lying β€” TS will let you, runtime will not

If you need this, you probably need a real validator (Zod, Valibot, io-ts).

Putting it together

type Status = "loading" 
"success"
"error";

function describe(s: Status): string { switch (s) { case "loading": return "⏳"; case "success": return "βœ…"; case "error": return "❌"; } }

describe("success"); // βœ… // describe("done"); // ❌ not assignable to Status