Skip to main content

Basic Types

Everything in TypeScript starts here: telling the compiler (or letting it figure out) what type a value is.

Type Annotations

An annotation is a colon followed by a type. You can annotate variables, parameters, and return values:

let age: number = 29;
let name: string = "Rizwan";
let isOnline: boolean = true;

function double(n: number): number {
return n * 2;
}

Once a variable has a type, assigning anything else is a compile error:

let age: number = 29;

age = 30; // ✅ fine
age = "30"; // ❌ Type 'string' is not assignable to type 'number'

Type Inference

Here's the thing beginners miss: you rarely need to annotate variables. TypeScript infers the type from the value:

let age = 29; // inferred as number
let name = "Rizwan"; // inferred as string
let isOnline = true; // inferred as boolean

age = "30"; // ❌ still an error — the inference is just as strict

My rule of thumb from production code: annotate function parameters and return types, let inference handle local variables. Parameters can't be inferred (TypeScript can't know what callers will pass), and explicit return types catch bugs inside the function. Everything else, let the compiler do the work.

function calculateTotal(price: number, quantity: number): number {
const subtotal = price * quantity; // inferred: number
const tax = subtotal * 0.17; // inferred: number
return subtotal + tax;
}

Arrays

Two equivalent syntaxes — pick one and be consistent (I use the first):

const scores: number[] = [85, 92, 78];
const names: Array<string> = ["Ali", "Sara", "Ahmed"];

scores.push(90); // ✅
scores.push("high"); // ❌ Argument of type 'string' is not assignable to 'number'

Inference works here too:

const scores = [85, 92, 78]; // inferred as number array

const found = scores.find((s) => s > 90); // inferred: number or undefined

That last inference is important — find may not find anything, and TypeScript forces you to handle the undefined case. That's a bug JavaScript would let slide.

Tuples

A tuple is an array with a fixed length and a known type at each position:

type Coordinate = [number, number];
const lahore: Coordinate = [31.5204, 74.3587];

type HttpResponse = [number, string];
const response: HttpResponse = [404, "Not Found"];

const bad: Coordinate = [31.5204, "74.3587"]; // ❌ string at position 1

You've already used tuples if you've used React — useState returns one: the value at index 0 and a setter function at index 1.

any vs unknown

any turns the type checker off for a value. Anything goes:

let data: any = JSON.parse('{"name": "Rizwan"}');

data.foo.bar.baz; // ✅ compiles fine... crashes at runtime
data.toUpperCase(); // ✅ compiles fine... crashes at runtime

any is the escape hatch that quietly spreads through a codebase and takes your type safety with it. When you genuinely don't know the type, use unknown instead:

let data: unknown = JSON.parse('{"name": "Rizwan"}');

data.toUpperCase(); // ❌ 'data' is of type 'unknown'

// You must check before using it:
if (typeof data === "string") {
data.toUpperCase(); // ✅ safe — TypeScript knows it's a string here
}

unknown says "I don't know what this is yet — prove it before using it." That's exactly the right attitude toward API responses, user input, and anything from the outside world.

Union Types

A union says a value can be one of several types, using the pipe symbol:

let id: string | number;

id = 42; // ✅
id = "abc-42"; // ✅
id = true; // ❌

Unions show up everywhere in real code — especially "value or nothing":

function findUser(id: number): string | undefined {
const users: Record<number, string> = { 1: "Rizwan", 2: "Sara" };
return users[id];
}

Literal Types

A literal type is a type with exactly one allowed value. On their own they look useless, but in a union they're one of TypeScript's best features:

type OrderStatus = "pending" | "shipped" | "delivered" | "cancelled";

function updateOrder(orderId: number, status: OrderStatus): void {
console.log(`Order ${orderId} is now ${status}`);
}

updateOrder(101, "shipped"); // ✅
updateOrder(101, "shiped"); // ❌ typo caught at compile time!

In plain JavaScript, that typo silently creates an order with a status no code ever checks for. I've debugged exactly this in a production order system — a string union would have caught it before the code review.

Narrowing with typeof

When you have a union, you can't use type-specific methods until you narrow it. The most common tool is a plain typeof check — TypeScript understands your if statements:

function formatId(id: string | number): string {
if (typeof id === "string") {
// in this branch, id is string
return id.toUpperCase();
}
// TypeScript knows: only number is left
return `ID-${id.toFixed(0)}`;
}

console.log(formatId("abc-42")); // "ABC-42"
console.log(formatId(42)); // "ID-42"

This is called a type guard. Nothing magical — it's the same typeof you know from JavaScript, but the compiler tracks it and adjusts the type inside each branch. Narrowing also works with Array.isArray, equality checks, and the in operator:

function describe(value: string | string[] | null): string {
if (value === null) {
return "nothing";
}
if (Array.isArray(value)) {
return `${value.length} items`;
}
return `one item: ${value}`;
}

Recap

  • Annotate parameters and return types; let inference handle locals
  • Arrays are number[], tuples fix the length and per-position types
  • Avoid any; use unknown and narrow it before use
  • Unions (string | number) plus literal types ("pending" | "shipped") model real-world data precisely
  • typeof, Array.isArray, and equality checks narrow unions safely

Next: shaping structured data with object types and interfaces.