Skip to content

1. Types & Interfaces


1. Basic Types

ts
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

ts
let city = "Kolkata";   // inferred as string
// let city: string = "Kolkata";   // redundant

Annotate function parameters and return types and exported values. Let TypeScript infer local variables.

Arrays and Tuples

ts
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

ts
let value: any = "hello";
value.foo.bar.baz();   // compiles, crashes at runtime

any is contagious. It spreads through every expression it touches.

unknown — The Safe any

ts
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

ts
function fail(msg: string): never {
  throw new Error(msg);   // never returns
}

type Impossible = string & number;   // never

Its practical use is exhaustiveness checking:

ts
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

anyunknownnever
Assignable from anythingYesYesNo
Assignable to anythingYesNoYes
Requires narrowingNoYes
SafeNoYesYes

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

ts
interface User {
  id: number;
  name: string;
}

type UserType = {
  id: number;
  name: string;
};

Only interface Can Be Reopened (Declaration Merging)

ts
interface Window {
  myApp: { version: string };
}
// merges with the built-in Window

Only type Can Do Unions, Tuples and Primitives

ts
type Status = "idle" | "loading" | "error";
type Point = [number, number];
type ID = string | number;
type Handler = (e: Event) => void;

Extension Syntax

ts
interface Admin extends User {
  role: string;
}

type AdminType = UserType & { role: string };

Comparison

Featureinterfacetype
Object shapesYesYes
UnionsNoYes
Tuples / primitivesNoYes
Extendsextends&
Declaration mergingYesNo
Computed / mapped typesNoYes
Implements in a classYesYes (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"

ts
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"

ts
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.

ts
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

ts
type Direction = "up" | "down" | "left" | "right";
type Dice = 1 | 2 | 3 | 4 | 5 | 6;

let dir: Direction = "up";
// dir = "diagonal";   // Error

Massively better than string for props, actions and status fields — you get autocomplete and typo protection.

as const

ts
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:

ts
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

ts
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

ts
interface Config {
  readonly options: { debug: boolean };
}

config.options = {};            // Error
config.options.debug = true;    // Allowed — only the top level is readonly

Use Readonly<T> for one level, or a DeepReadonly helper for more.

? vs | undefined

ts
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 };  // OK

Under exactOptionalPropertyTypes, ? and | undefined become properly distinct.


7. Functions

ts
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

ts
type Callback = (error: Error | null, data?: string) => void;
type Predicate<T> = (item: T) => boolean;

Overloads

ts
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

ts
type Fn = () => void;

const f: Fn = () => 42;   // Allowed — the return value is just ignored

This 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

ts
enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE",
}

Why Most Codebases Avoid Enums

  • Numeric enums are not type-safeStatus.Active = 5 compiles even if 5 isn't a member
  • Enums emit real JavaScript, so they are not fully erasable
  • const enum is banned under isolatedModules, which most bundlers require

The Modern Alternative

ts
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

ts
const input = document.getElementById("email") as HTMLInputElement;
input.value = "test";

Assertions Are Not Conversions

ts
const x = "hello" as unknown as number;   // lies to the compiler
x.toFixed(2);                              // crashes at runtime

An assertion says "trust me". If you are wrong, TypeScript cannot help you.

Non-Null Assertion — !

ts
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:

ts
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.

ts
const config = {
  mode: "dark",
  retries: 3,
} satisfies Config;

config.mode;   // still the literal "dark", not string

With : 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

json
{
  "compilerOptions": {
    "strict": true
  }
}

Turns on:

FlagEffect
strictNullChecksnull and undefined are not assignable to other types
noImplicitAnyError on a parameter TypeScript can't infer
strictFunctionTypesContravariant parameter checking
strictBindCallApplyType-checks bind, call, apply
strictPropertyInitializationClass fields must be initialised
noImplicitThisError on an untyped this
useUnknownInCatchVariablescatch (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.

© 2025 DDocs · Dipak's Documentation Guide