Master the types you use daily — primitives and inference, arrays, tuples, objects with optional and readonly properties, enums, and any, unknown, void, never.
Why: every value has a type. Annotate where TypeScript cannot guess (function parameters, empty variables) and let inference handle the rest. Note: hover any variable in your editor to see what was inferred.
// Explicit annotations
let username: string = 'Alice';
let age: number = 30;
let active: boolean = true;
// Inference — TypeScript already knows, no annotation needed
let city = 'Cairo'; // string
let score = 9.5; // number
city = 'Alexandria'; // ok
// city = 42; // Error: Type 'number' is not assignable to 'string'
// null and undefined are their own types under strict mode
let nickname: string | null = null;
nickname = 'Ali';
// Parameters always need annotations — inference has nothing to go on
function greet(name: string) {
return `Hello, ${name}!`; // return type 'string' is inferred
}
console.log(greet(username), city, score, active, nickname);Why: arrays hold any number of one type; tuples have a fixed length with a specific type per position — perfect for pairs and small fixed records.
// Arrays — every element has the same type
const tags: string[] = ['ts', 'js'];
tags.push('node'); // ok
// tags.push(42); // Error: number is not a string
// Tuples — fixed length, fixed type per position
const point: [number, number] = [10, 20];
const entry: [string, number, boolean?] = ['clicks', 42]; // optional 3rd slot
// Destructuring keeps the types
const [x, y] = point; // x: number, y: number
// Named tuple elements document intent
type HttpResponse = [status: number, body: string];
const res: HttpResponse = [200, 'OK'];
console.log(tags, x + y, entry, res);Why: object types describe exact shapes. ? marks a property that may be missing; readonly blocks reassignment after creation. The compiler forces you to handle the optional case.
// Describe a shape inline…
let user: { name: string; age: number; email?: string } = {
name: 'Alice',
age: 30,
}; // email is optional — fine to leave out
// …or with a reusable alias
type Product = {
id: number;
title: string;
readonly sku: string; // cannot change after creation
};
const book: Product = { id: 1, title: 'TS Handbook', sku: 'TS-001' };
// book.sku = 'TS-002'; // Error: sku is read-only
// Optional access needs ?. or a guard
console.log(user.email?.toLowerCase() ?? 'no email');
console.log(book.title);Why: enums name a fixed set of related constants. String enums stay readable in logs and JSON. Note: a union of string literals often does the same job with zero runtime code — you will see both in real codebases.
// Numeric enum — auto-increments from 0
enum Direction { Up, Down, Left, Right }
const move: Direction = Direction.Up; // 0
// String enum — readable when logged or serialized
enum Status {
Active = 'ACTIVE',
Banned = 'BANNED',
}
function describe(status: Status) {
switch (status) {
case Status.Active: return 'can log in';
case Status.Banned: return 'blocked';
}
}
console.log(move, describe(Status.Active));
// The lightweight alternative — a literal union:
type StatusLite = 'active' | 'banned';
const s: StatusLite = 'active';
console.log(s);Why: any switches the type checker off — avoid it. unknown is the safe "I don't know yet": you must narrow it before use. void marks functions that return nothing; never marks code paths that cannot happen.
// any — no checking at all. Bugs slip straight through.
let anything: any = 4;
// anything.toUpperCase(); // compiles fine — crashes at runtime!
// unknown — you must prove the type before using it
let input: unknown = JSON.parse('"hello"');
// input.toUpperCase(); // Error: input is unknown
if (typeof input === 'string') {
console.log(input.toUpperCase()); // ok — narrowed to string
}
// void — a function with nothing to return
function log(msg: string): void {
console.log(msg);
}
// never — this function never returns at all
function fail(msg: string): never {
throw new Error(msg);
}
log('done');