What is TypeScript and Why Use It?
TypeScript is JavaScript with an optional static type system, developed by Microsoft. It compiles to plain JavaScript — browsers and Node.js run the JS output, not TypeScript directly.
Analogy: JavaScript is like writing on paper with no spellcheck.
TypeScript adds spellcheck + grammar check that runs before you
hand in the document — catching errors before they reach the reader.
Why it exists: As JavaScript codebases grew to millions of lines, bugs from wrong types became expensive. TypeScript moves these errors from runtime (production crashes) to compile time (editor/build errors).
// JavaScript — no error until runtime
function double(n) {
return n * 2;
}
double("5"); // returns "55" — silent bug!
// TypeScript — error at development time
function double(n: number): number {
return n * 2;
}
double("5"); // ❌ Argument of type 'string' is not assignable to type 'number'
Industry adoption: TypeScript is now the default for most major JavaScript projects — React, Next.js, Angular, Vue 3, Node.js backends, and virtually every FAANG JavaScript codebase.
Basic Types
// Primitives
let name: string = "Alice";
let age: number = 25; // includes floats: 3.14
let active: boolean = true;
let nothing: null = null;
let missing: undefined = undefined;
let unique: symbol = Symbol("id");
// Arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"]; // generic syntax (equivalent)
// Tuple — fixed-length, fixed-type array
let point: [number, number] = [10, 20];
let entry: [string, number] = ["age", 25];
// Any — opts out of type checking (avoid; use only when migrating)
let data: any = fetchUnknownData();
// Unknown — safer any — must type-check before using
let input: unknown = getUserInput();
if (typeof input === "string") {
input.toUpperCase(); // ✅ TS knows it's a string here
}
// Never — function that never returns (throws or infinite loops)
function fail(msg: string): never {
throw new Error(msg);
}
// Void — function that returns nothing
function log(msg: string): void {
console.log(msg);
}
Type Inference
TypeScript infers types automatically — you don't need to annotate everything:
// TypeScript infers these types — no annotation needed
const count = 0; // type: number
const name = "Alice"; // type: string
const nums = [1, 2, 3]; // type: number[]
const user = { id: 1, name: "Alice" }; // type: { id: number; name: string }
// Annotate only when inference isn't enough
const items: string[] = []; // inference would give never[] without the annotation
Interfaces vs Type Aliases
Both define the shape of an object. Prefer interfaces for objects, types for unions/intersections.
// Interface
interface User {
id: number;
name: string;
email?: string; // optional property (may be undefined)
readonly createdAt: Date; // cannot be reassigned after creation
}
// Type alias
type UserID = number;
type Status = "active" | "inactive" | "pending"; // union type
// Interfaces can be extended (open for extension — good for libraries)
interface AdminUser extends User {
role: "admin" | "super-admin";
permissions: string[];
}
// Type aliases use intersection for extension
type AdminUser = User & {
role: "admin";
};
// Interface merging — same name merges automatically
interface Window {
myCustomProp: string; // adds to the existing Window interface
}
Key difference: Interfaces can be re-declared to merge; type aliases cannot. Both support most use cases — pick one and be consistent.
Union Types & Discriminated Unions
// Union — value can be one of several types
type StringOrNumber = string | number;
function format(val: StringOrNumber): string {
if (typeof val === "string") return val.toUpperCase(); // type narrowing
return val.toFixed(2);
}
// Discriminated unions — a common field (discriminant) differentiates cases
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "rectangle": return shape.width * shape.height;
case "triangle": return 0.5 * shape.base * shape.height;
}
// TypeScript knows the switch is exhaustive — no default needed
}
Discriminated unions are the TypeScript equivalent of algebraic data types. They make switch statements exhaustive and type-safe.
Generics
Generics let you write functions and classes that work with any type while still being type-safe.
// Without generics: returns any — loses type info
function first(arr: any[]): any { return arr[0]; }
// With generics: preserves the type
function first<T>(arr: T[]): T | undefined { return arr[0]; }
const n = first([1, 2, 3]); // TypeScript knows n is number | undefined
const s = first(["a", "b"]); // TypeScript knows s is string | undefined
// Generic constraints — T must have certain properties
function getLength<T extends { length: number }>(val: T): number {
return val.length;
}
getLength("hello"); // ✅ strings have .length
getLength([1, 2, 3]); // ✅ arrays have .length
getLength(42); // ❌ numbers don't have .length
// Generic interfaces
interface Repository<T> {
findById(id: number): Promise<T | null>;
findAll(): Promise<T[]>;
save(entity: T): Promise<T>;
delete(id: number): Promise<void>;
}
// Usage
interface User { id: number; name: string; }
class UserRepository implements Repository<User> {
async findById(id: number): Promise<User | null> { /* ... */ }
async findAll(): Promise<User[]> { /* ... */ }
async save(user: User): Promise<User> { /* ... */ }
async delete(id: number): Promise<void> { /* ... */ }
}
Utility Types
TypeScript ships built-in generic types for common transformations:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Partial<T> — makes all properties optional (great for update payloads)
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; password?: string }
// Required<T> — opposite: makes all required
type StrictUser = Required<Partial<User>>;
// Readonly<T> — prevents mutation
const user: Readonly<User> = { id: 1, name: "Alice", email: "a@a.com", password: "x" };
user.name = "Bob"; // ❌ Cannot assign to 'name' because it is a read-only property
// Pick<T, K> — select specific properties
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }
// Omit<T, K> — exclude specific properties
type PublicUser = Omit<User, "password">;
// { id: number; name: string; email: string }
// Record<K, V> — object with keys of type K and values of type V
type PageViews = Record<string, number>;
const views: PageViews = { home: 100, about: 50 };
// ReturnType<T> — extract return type of a function
function createUser() { return { id: 1, name: "Alice" }; }
type CreatedUser = ReturnType<typeof createUser>; // { id: number; name: string }
// Parameters<T> — extract parameter types of a function
type CreateParams = Parameters<typeof createUser>; // []
Type Guards & Narrowing
TypeScript narrows types based on conditional checks:
// typeof — for primitives
function process(val: string | number) {
if (typeof val === "string") {
val.toUpperCase(); // TS knows it's string here
} else {
val.toFixed(2); // TS knows it's number here
}
}
// instanceof — for class instances
function formatError(err: Error | string) {
if (err instanceof Error) {
return err.message;
}
return err;
}
// in — check if property exists
type Dog = { bark: () => void };
type Cat = { meow: () => void };
function makeSound(animal: Dog | Cat) {
if ("bark" in animal) animal.bark();
else animal.meow();
}
// Custom type guards — function that returns a type predicate
function isUser(val: unknown): val is User {
return typeof val === "object" && val !== null && "id" in val && "name" in val;
}
const data: unknown = JSON.parse(response);
if (isUser(data)) {
console.log(data.name); // ✅ TypeScript trusts isUser's return type
}
Enums
// Numeric enum (values are 0, 1, 2...)
enum Direction { North, South, East, West }
const dir: Direction = Direction.North; // 0
// String enum (recommended — values are meaningful in logs/debugging)
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING",
}
const status: Status = Status.Active; // "ACTIVE"
// Const enum (erased at compile time — just numeric literals in output)
const enum Flags { Read = 1, Write = 2, Execute = 4 }
Prefer string enums for readability. Or use as const objects instead:
const STATUS = { Active: "ACTIVE", Inactive: "INACTIVE" } as const;
type Status = typeof STATUS[keyof typeof STATUS]; // "ACTIVE" | "INACTIVE"
TypeScript with React
// Props typing
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary";
disabled?: boolean;
children?: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({ label, onClick, variant = "primary", disabled = false }) => (
<button className={`btn btn-${variant}`} onClick={onClick} disabled={disabled}>
{label}
</button>
);
// useState with type
const [users, setUsers] = React.useState<User[]>([]);
const [loading, setLoading] = React.useState<boolean>(false);
// useRef with type
const inputRef = React.useRef<HTMLInputElement>(null);
Common Interview Questions
Practice
- Basic: Type a
Stack<T>class withpush(item: T),pop(): T | undefined,peek(): T | undefined, andisEmpty(): boolean. - Generics: Implement a typed
groupBy<T, K>(items: T[], keyFn: (item: T) => K): Map<K, T[]>utility. - Utility Types: Given a
UserForminterface with all required fields, deriveUserFormDraft = Partial<UserForm>andUserFormReadonly = Readonly<UserForm>. Use them in a form component. - Discriminated Unions: Model an API response type:
Success<T>with data,Loading, andErrorwith message. Write arenderState<T>(state: ApiState<T>): stringfunction that's exhaustive.
Next: React — building component-based UIs.