TypeScript types describe the shape of values — basic types (string, number, boolean), object shapes (interface and type alias), union types (A | B), and intersection types (A & B) — all erased at compile time, leaving plain JavaScript.
TypeScript adds a type system on top of JavaScript — you annotate variables, parameters, and return values with types, and the TypeScript compiler catches type errors before your code ever runs.
Basic type annotations
// Type annotations — added with a colon after the variable name
let name: string = "Priya";
let age: number = 28;
let active: boolean = true;
let nothing: null = null;
let unknown: undefined = undefined;
// TypeScript infers types when you initialise
let inferred = "Hello"; // type: string — no annotation needed
inferred = 42; // Error: Type 'number' is not assignable to type 'string'
// Arrays
let names: string[] = ["Priya", "Rahul"];
let scores: Array<number> = [95, 88, 72]; // generic syntax
// Tuple — fixed length, fixed types at each position
let pair: [string, number] = ["Priya", 95];
let rgb: [number, number, number] = [255, 128, 0];
// Union — one of several types
let id: string | number = "user-42";
id = 42; // also valid
// Literal types — only specific values
let direction: "north" | "south" | "east" | "west" = "north";
let statusCode: 200 | 404 | 500 = 200;
// any — opts out of type checking (avoid in new code)
let flexible: any = "hello";
flexible = 42; // no error — any disables checking
flexible.xyz(); // no error — dangerous
// unknown — safer alternative to any
let value: unknown = fetchFromAPI();
value.toUpperCase(); // Error — must narrow first
if (typeof value === "string") {
value.toUpperCase(); // OK — narrowed to string
}Interfaces
// Interface — describes the shape of an object
interface User {
readonly id: number; // readonly — cannot reassign after creation
name: string;
email: string;
age?: number; // optional — may or may not be present
role: "admin" | "user" | "guest";
}
// Any object matching this shape is a valid User (structural typing)
const priya: User = {
id: 1,
name: "Priya",
email: "priya@example.com",
role: "admin",
};
// priya.id = 99; // Error: Cannot assign to 'id' because it is a read-only property
// Interface for a function
interface Formatter {
(input: string, options?: { uppercase?: boolean }): string;
}
const format: Formatter = (input, opts) =>
opts?.uppercase ? input.toUpperCase() : input;
// Extending interfaces
interface AdminUser extends User {
permissions: string[];
lastLogin: Date;
}
// Merging — interfaces with the same name are merged (declaration merging)
interface Window {
myCustomProp: string; // adds to the existing Window interface
}Type aliases vs interfaces
// type alias — can name any type, including unions and primitives
type ID = string | number;
type Point = { x: number; y: number };
type Status = "pending" | "active" | "inactive";
// Intersection — combine types with &
type AdminUser = User & { permissions: string[] };
// Interface and type aliases are often interchangeable for objects
// Key differences:
// 1. type can express unions and intersections directly
// 2. interface supports declaration merging
// 3. interface extends uses extends keyword; type uses &
// 4. Error messages often mention interface names, not type alias names
// Prefer interface for object shapes (especially public APIs)
// Prefer type for unions, intersections, and complex type expressions
// Discriminated union — a union where a common field acts as a tag
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "rectangle": return s.width * s.height;
case "triangle": return 0.5 * s.base * s.height;
}
// TypeScript knows this switch is exhaustive — no default needed
}Type narrowing
TypeScript narrows the type of a variable inside conditional blocks — if you check typeof x === "string", TypeScript knows inside that block that x is a string. Multiple narrowing mechanisms exist.
function process(value: string | number | null) {
// typeof narrowing
if (typeof value === "string") {
console.log(value.toUpperCase()); // value: string
} else if (typeof value === "number") {
console.log(value.toFixed(2)); // value: number
} else {
console.log("null"); // value: null
}
}
// instanceof narrowing
function formatDate(d: Date | string): string {
if (d instanceof Date) {
return d.toISOString(); // d: Date
}
return d; // d: string
}
// in narrowing — check for a property
type Cat = { meow(): void };
type Dog = { bark(): void };
function makeSound(animal: Cat | Dog) {
if ("meow" in animal) {
animal.meow(); // animal: Cat
} else {
animal.bark(); // animal: Dog
}
}
// User-defined type guard — return type is a "type predicate"
function isString(value: unknown): value is string {
return typeof value === "string";
}
// Assertion function — throws if condition is false
function assert(condition: unknown, msg: string): asserts condition {
if (!condition) throw new Error(msg);
}Classes with TypeScript
class BankAccount {
readonly owner: string;
private balance: number;
protected bank: string;
constructor(owner: string, initial: number, bank: string) {
this.owner = owner;
this.balance = initial;
this.bank = bank;
}
deposit(amount: number): void {
if (amount <= 0) throw new Error("Amount must be positive");
this.balance += amount;
}
getBalance(): number { return this.balance; }
}
// Shorthand constructor — declare and assign in one
class Point {
constructor(
public readonly x: number,
public readonly y: number,
) {} // fields declared and assigned automatically
distanceTo(other: Point): number {
return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2);
}
}
const p1 = new Point(0, 0);
const p2 = new Point(3, 4);
p1.distanceTo(p2); // 5
// Abstract class
abstract class Animal {
abstract speak(): string;
describe(): string { return \`I am a \${this.constructor.name}\`; }
}
class Dog extends Animal {
speak(): string { return "Woof!"; }
}if (x instanceof MyInterface) is invalid — interfaces do not exist at runtime. Only classes, constructors, and JavaScript primitives work with instanceof. For runtime checks, use type guards (value is Type) or discriminated unions.class Cat { meow() {} } and class Dog { meow() {} } are interchangeable in TypeScript's type system even though they are different classes. Java and C# use nominal typing (names must match).any disables all type checking silently — unknown is almost always better. A variable of type any can be used in any way without errors — even calling methods that don't exist. unknown requires a type check (narrowing) before use. Always prefer unknown for values whose type you genuinely don't know.Structural typing and the Liskov Substitution Principle
TypeScript's structural type system is formally described in the TypeScript spec as "assignability." Type A is assignable to type B if A has all the required properties of B (with compatible types). This is the TypeScript formalisation of duck typing: if it has all the right properties, it is the right type. This means TypeScript interfaces are not sealed — a value can have more properties than required and still satisfy the interface. This differs from Go's structural typing only in that TypeScript types exist only at compile time.
Control flow analysis
TypeScript's type narrowing is powered by control flow analysis — the compiler traces all possible execution paths through branches, loops, and exception handlers. The type of a variable at any point is the union of types it could have on all paths that reach that point. This is why TypeScript can narrow inside if, after return, after throw, and in switch exhaustive patterns. The "never" type represents an unreachable code path — if a variable narrows to never, that branch is provably unreachable.