1. Types & Interfaces
1. Basic Types
let name: string = "Dipak";
let age: number = 25;
let isActive: boolean = true;
let nothing: null = null;
let notSet: undefined = undefined;
let big: bigint = 123n;
let sym: symbol = Symbol("id");Type Inference — Prefer It
let city = "Kolkata"; // inferred as string
// let city: string = "Kolkata"; // redundantAnnotate function parameters and return types and exported values. Let TypeScript infer local variables.
Arrays and Tuples
let nums: number[] = [1, 2, 3];
let names: Array<string> = ["a", "b"];
// Tuple — fixed length, fixed types per position
let point: [number, number] = [10, 20];
let entry: [string, number, boolean?] = ["a", 1];
// Named tuple — better error messages
let coord: [x: number, y: number] = [1, 2];Tuples are what useState returns: [T, Dispatch<SetStateAction<T>>].
2. any vs unknown vs never
A guaranteed interview question.
any — Turns Off TypeScript
let value: any = "hello";
value.foo.bar.baz(); // compiles, crashes at runtimeany is contagious. It spreads through every expression it touches.
unknown — The Safe any
let value: unknown = "hello";
// value.toUpperCase(); // Error — must narrow first
if (typeof value === "string") {
value.toUpperCase(); // OK
}Anything is assignable to unknown, but unknown is assignable to nothing without narrowing.
never — Impossible
function fail(msg: string): never {
throw new Error(msg); // never returns
}
type Impossible = string & number; // neverIts practical use is exhaustiveness checking:
type Status = "idle" | "loading" | "done";
function label(s: Status) {
switch (s) {
case "idle": return "Idle";
case "loading": return "Loading";
case "done": return "Done";
default: {
const exhaustive: never = s; // compile error if a case is missing
return exhaustive;
}
}
}Add "error" to Status and this file fails to compile until you handle it. That is the pattern interviewers want to see.
Comparison
any | unknown | never | |
|---|---|---|---|
| Assignable from anything | Yes | Yes | No |
| Assignable to anything | Yes | No | Yes |
| Requires narrowing | No | Yes | — |
| Safe | No | Yes | Yes |
Interview Point
Use unknown at every boundary where data enters your app — JSON.parse, API responses, catch blocks. Then narrow it. any at a boundary means the rest of your types are a fiction.
3. type vs interface
The most-asked TypeScript question.
Both Can Describe an Object
interface User {
id: number;
name: string;
}
type UserType = {
id: number;
name: string;
};Only interface Can Be Reopened (Declaration Merging)
interface Window {
myApp: { version: string };
}
// merges with the built-in WindowOnly type Can Do Unions, Tuples and Primitives
type Status = "idle" | "loading" | "error";
type Point = [number, number];
type ID = string | number;
type Handler = (e: Event) => void;Extension Syntax
interface Admin extends User {
role: string;
}
type AdminType = UserType & { role: string };Comparison
| Feature | interface | type |
|---|---|---|
| Object shapes | Yes | Yes |
| Unions | No | Yes |
| Tuples / primitives | No | Yes |
| Extends | extends | & |
| Declaration merging | Yes | No |
| Computed / mapped types | No | Yes |
| Implements in a class | Yes | Yes (object types) |
The Answer That Works
"Interface for object shapes that might be extended, especially public API surfaces and anything a library consumer might augment. Type for unions, tuples, function signatures and anything computed. In practice most teams pick one and stay consistent — the difference rarely matters."
4. Union and Intersection Types
Union — | — "one of these"
type Result = string | number;
function format(id: string | number) {
if (typeof id === "string") return id.toUpperCase();
return id.toFixed(2);
}Intersection — & — "all of these at once"
type Timestamps = { createdAt: Date; updatedAt: Date };
type Post = { title: string } & Timestamps;
// { title, createdAt, updatedAt }Interview Trap
Union is not "either type's properties". You can only access members present on every member of the union until you narrow.
type A = { a: string; shared: string };
type B = { b: string; shared: string };
function f(x: A | B) {
x.shared; // OK — on both
// x.a; // Error — might be B
}5. Literal Types
type Direction = "up" | "down" | "left" | "right";
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
let dir: Direction = "up";
// dir = "diagonal"; // ErrorMassively better than string for props, actions and status fields — you get autocomplete and typo protection.
as const
const config = { mode: "dark", retries: 3 };
// inferred: { mode: string; retries: number }
const config = { mode: "dark", retries: 3 } as const;
// inferred: { readonly mode: "dark"; readonly retries: 3 }Common pattern — derive a union from an array:
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof ROLES[number]; // "admin" | "editor" | "viewer"One source of truth for both the runtime array and the type.
6. Optional, Readonly and Index Signatures
interface User {
id: number;
name: string;
email?: string; // optional — string | undefined
readonly createdAt: Date; // cannot be reassigned
[key: string]: unknown; // index signature
}readonly Is Shallow
interface Config {
readonly options: { debug: boolean };
}
config.options = {}; // Error
config.options.debug = true; // Allowed — only the top level is readonlyUse Readonly<T> for one level, or a DeepReadonly helper for more.
? vs | undefined
interface A { x?: number } // may be absent entirely
interface B { x: number | undefined } // must be present, may be undefined
const a: A = {}; // OK
const b: B = {}; // Error — x is required
const b2: B = { x: undefined }; // OKUnder exactOptionalPropertyTypes, ? and | undefined become properly distinct.
7. Functions
function add(a: number, b: number): number {
return a + b;
}
const multiply = (a: number, b: number): number => a * b;
// Optional and default parameters
function greet(name: string, greeting = "Hello", title?: string): string {
return `${greeting}, ${title ?? ""} ${name}`;
}
// Rest parameters
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}Function Type
type Callback = (error: Error | null, data?: string) => void;
type Predicate<T> = (item: T) => boolean;Overloads
function parse(input: string): object;
function parse(input: string, raw: true): string;
function parse(input: string, raw?: boolean): object | string {
return raw ? input : JSON.parse(input);
}The implementation signature is not callable from outside — only the overloads are.
void vs undefined
type Fn = () => void;
const f: Fn = () => 42; // Allowed — the return value is just ignoredThis is why array.forEach(x => arr.push(x)) compiles even though push returns a number. A void return type means "the caller must not rely on the return value", not "must return nothing".
8. Enums vs Union Literals
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
}Why Most Codebases Avoid Enums
- Numeric enums are not type-safe —
Status.Active = 5compiles even if 5 isn't a member - Enums emit real JavaScript, so they are not fully erasable
const enumis banned underisolatedModules, which most bundlers require
The Modern Alternative
const Status = {
Active: "ACTIVE",
Inactive: "INACTIVE",
} as const;
type Status = typeof Status[keyof typeof Status]; // "ACTIVE" | "INACTIVE"Zero runtime cost beyond a plain object, and fully type-safe.
Interview Point
Knowing why teams avoid enums scores better than knowing the syntax. TypeScript 5.0 also made all enums union enums, which fixed some but not all of the problems.
9. Type Assertions
const input = document.getElementById("email") as HTMLInputElement;
input.value = "test";Assertions Are Not Conversions
const x = "hello" as unknown as number; // lies to the compiler
x.toFixed(2); // crashes at runtimeAn assertion says "trust me". If you are wrong, TypeScript cannot help you.
Non-Null Assertion — !
const el = document.querySelector("#app")!; // "it's definitely not null"Convenient, and the source of a lot of production null errors. Prefer an explicit check:
const el = document.querySelector("#app");
if (!el) throw new Error("#app not found");satisfies (TypeScript 4.9+)
The best of both worlds — validate against a type without widening the inferred one.
const config = {
mode: "dark",
retries: 3,
} satisfies Config;
config.mode; // still the literal "dark", not stringWith : Config you would lose the literal. With satisfies you keep it and get the check. Worth mentioning — it signals you follow modern TypeScript.
10. strict Mode
{
"compilerOptions": {
"strict": true
}
}Turns on:
| Flag | Effect |
|---|---|
strictNullChecks | null and undefined are not assignable to other types |
noImplicitAny | Error on a parameter TypeScript can't infer |
strictFunctionTypes | Contravariant parameter checking |
strictBindCallApply | Type-checks bind, call, apply |
strictPropertyInitialization | Class fields must be initialised |
noImplicitThis | Error on an untyped this |
useUnknownInCatchVariables | catch (e) is unknown, not any |
Interview Point
strictNullChecks is the flag that provides most of TypeScript's real-world value — it is what eliminates "cannot read property of undefined". Running without strict gives you autocomplete and very little safety. Always turn it on, from day one on a new project.
Other flags worth adding: noUncheckedIndexedAccess (makes arr[0] return T | undefined, which is the truth) and noUnusedLocals.