Skip to main content
TypeScript · lesson 21 of 21
TypeScript intermediate Lesson 21 of 21

TypeScript Interview Prep

30 TypeScript interview questions on types, generics, narrowing, utility, conditional and mapped types, each answered with real tsc and tsx output.

By SkillByExample · Updated

Every answer below is backed by a file that was actually compiled and run, with TypeScript 6.0.3 and Node 22, in a folder whose package.json has "type": "module". npx tsx file.ts runs a file; npx tsc --noEmit --module nodenext file.ts type-checks it. TypeScript 6 is strict by default. Where a question asks what a type resolves to, the file assigns a deliberately wrong value so the compiler error prints the answer.

Q1: What is TypeScript, and what happens to the types at runtime?

TypeScript is JavaScript plus a static type system. The compiler checks the types, then erases them: the emitted JavaScript contains none of them.

interface User { name: string }
const user: User = { name: "Ada" };
const id = <T,>(x: T): T => x;
console.log(id(user).name);
$ npx tsc --module nodenext --outDir js q1.ts
$ cat js/q1.js
const user = { name: "Ada" };
const id = (x) => x;
console.log(id(user).name);
export {};
$ node js/q1.js
Ada

The interface, the annotation and the type parameter are gone. That’s why you can’t instanceof an interface, and why data from outside the program needs runtime checks.

Q2: What is the difference between any and unknown?

Both accept any value. any switches checking off; unknown makes you narrow before use.

const a: any = "hello";
const u: unknown = "hello";

a.toFixed(2);
u.toUpperCase();
if (typeof u === "string") console.log(u.toUpperCase());
$ npx tsc --noEmit --module nodenext q2.ts
q2.ts(5,1): error TS18046: 'u' is of type 'unknown'.

Only the unknown line is flagged — even though a.toFixed(2) would crash at runtime, because a string has no toFixed. Inside the typeof check, u is a string.

Q3: What does strict: true enable?

A family of checks: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, alwaysStrict and strictBuiltinIteratorReturn. In TypeScript 6 it’s on by default.

function greet(name) {
  return "hi " + name;
}
let title: string = null;
class Box { size: number }
try {} catch (e) { console.log(e.message); }
greet.call(undefined, 1, 2);
$ npx tsc --noEmit --module nodenext q3.ts
q3.ts(1,16): error TS7006: Parameter 'name' implicitly has an 'any' type.
q3.ts(4,5): error TS2322: Type 'null' is not assignable to type 'string'.
q3.ts(5,13): error TS2564: Property 'size' has no initializer and is not definitely assigned in the constructor.
q3.ts(6,32): error TS18046: 'e' is of type 'unknown'.
q3.ts(7,26): error TS2554: Expected 2 arguments, but got 3.
$ npx tsc --noEmit --module nodenext --strict false q3.ts

Five errors with the default, none with --strict false. Every one of them is a latent runtime bug.

Q4: What is the difference between interface and type?

Both describe object shapes. Interfaces can be re-opened (declaration merging) and use extends; type aliases can name any type — unions, tuples, mapped and conditional types — but can’t be merged.

interface Point { x: number }
interface Point { y: number }
const p: Point = { x: 1 };

type Status = "on" | "off";
type Status = "unknown";
$ npx tsc --noEmit --module nodenext q4.ts
q4.ts(3,7): error TS2741: Property 'y' is missing in type '{ x: number; }' but required in type 'Point'.
q4.ts(5,6): error TS2300: Duplicate identifier 'Status'.
q4.ts(6,6): error TS2300: Duplicate identifier 'Status'.

The two Point declarations merged, so y is now required. The second Status is simply a duplicate.

Q5: What are generics? Give a practical example.

A type parameter links types across a signature, so the output type follows the input type without any.

function first<T>(items: T[]): T | undefined {
  return items[0];
}

const n = first([1, 2, 3]);
const s = first(["a", "b"]);
console.log(n?.toFixed(1), s?.toUpperCase());

const wrong: string = first([1, 2, 3]);
$ npx tsc --noEmit --module nodenext q5.ts
q5.ts(9,7): error TS2322: Type 'number | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'.

T was inferred per call. | undefined is there because the array might be empty.

Q6: What is a type guard?

An expression or function that narrows a type inside a branch. A user-defined guard returns a type predicate, value is T.

function isError(value: unknown): value is Error {
  return value instanceof Error;
}

function describe(value: unknown): string {
  if (isError(value)) return `Error: ${value.message}`;
  if (typeof value === "string") return `string: ${value}`;
  return `other: ${JSON.stringify(value)}`;
}

console.log(describe(new RangeError("too big")));
console.log(describe("plain"));
console.log(describe({ code: 7 }));
$ npx tsc --noEmit --module nodenext q6.ts
$ npx tsx q6.ts
Error: too big
string: plain
other: {"code":7}

The compiler trusts the predicate — it doesn’t check that the function body actually proves it.

Q7: Explain discriminated unions.

A union whose members share a literal property. Checking that property narrows the whole object, and a switch over it can be exhaustive.

type Result<T> =
  | { status: "success"; data: T }
  | { status: "error"; message: string }
  | { status: "loading" };

function render<T>(r: Result<T>): string {
  switch (r.status) {
    case "success": return `data: ${JSON.stringify(r.data)}`;
    case "error": return `error: ${r.message}`;
    case "loading": return "loading...";
  }
}

console.log(render({ status: "success", data: [1, 2] }));
console.log(render({ status: "error", message: "timeout" }));
console.log(render({ status: "error", data: 1 }));
$ npx tsc --noEmit --module nodenext q7.ts
q7.ts(16,39): error TS2353: Object literal may only specify known properties, and 'data' does not exist in type '{ status: "error"; message: string; }'.

An "error" object can’t carry data. With the bad line removed:

$ npx tsx q7.ts
data: [1,2]
error: timeout

Q8: What does keyof do?

keyof T is the union of T’s keys. With an indexed access T[K] it types property lookups exactly.

interface User { id: number; name: string; email: string }
const user: User = { id: 1, name: "Ada", email: "ada@example.com" };

function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const id: number = get(user, "id");
const name: string = get(user, "name");
console.log(id, name);
const key: keyof User = "age";
$ npx tsc --noEmit --module nodenext q8.ts
q8.ts(11,7): error TS2322: Type '"age"' is not assignable to type 'keyof User'.

Q9: What is typeof in a type position?

It turns a value into its type. The runtime typeof returns a string like "object"; the type-level one returns the full TypeScript type.

const config = { host: "localhost", port: 3000 };
type Config = typeof config;

function createUser() {
  return { id: 1, name: "Ada" };
}
type User = ReturnType<typeof createUser>;

console.log(typeof config);
const c: Config = { host: "x" };
const u: User = { id: "1", name: "Ada" };
$ npx tsc --noEmit --module nodenext q9.ts
q9.ts(10,7): error TS2741: Property 'port' is missing in type '{ host: string; }' but required in type '{ host: string; port: number; }'.
q9.ts(11,19): error TS2322: Type 'string' is not assignable to type 'number'.
$ npx tsx q9.ts
object

Q10: What are mapped types?

{ [K in keyof T]: ... } builds a new object type by transforming each property of T.

type Nullable<T> = { [K in keyof T]: T[K] | null };
type Getters<T> = { [K in keyof T & string as `get${Capitalize<K>}`]: () => T[K] };

interface User { name: string; age: number }

const u: Nullable<User> = { name: null, age: 36 };
const g: Getters<User> = {};
console.log(u);
$ npx tsc --noEmit --module nodenext q10.ts
q10.ts(7,7): error TS2739: Type '{}' is missing the following properties from type 'Getters<User>': getName, getAge

The as clause renames keys — here producing getName and getAge. Partial, Readonly and Record are mapped types too.

Q11: What is infer?

Inside a conditional type, infer X captures part of the type being matched.

type Unpack<T> = T extends (infer U)[] ? U : T;
type Ret<T> = T extends (...args: any[]) => infer R ? R : never;

const a: Unpack<string[]> = 1;
const b: Unpack<number> = "x";
const c: Ret<() => boolean> = "x";
$ npx tsc --noEmit --module nodenext q11.ts
q11.ts(4,7): error TS2322: Type 'number' is not assignable to type 'string'.
q11.ts(5,7): error TS2322: Type 'string' is not assignable to type 'number'.
q11.ts(6,7): error TS2322: Type 'string' is not assignable to type 'boolean'.

The errors name the resolved types: string, number (non-arrays pass through), and boolean.

Q12: Implement Partial<T> from scratch.

type MyPartial<T> = { [K in keyof T]?: T[K] };

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

const patch: MyPartial<User> = { name: "Ada" };
const empty: MyPartial<User> = {};
const bad: MyPartial<User> = { id: "7" };
const extra: MyPartial<User> = { nickname: "A" };
console.log(patch, empty);
$ npx tsc --noEmit --module nodenext q12.ts
q12.ts(7,32): error TS2322: Type 'string' is not assignable to type 'number'.
q12.ts(8,34): error TS2353: Object literal may only specify known properties, and 'nickname' does not exist in type 'MyPartial<User>'.

Every property becomes optional, but still keeps its type — and unknown properties are still rejected. A follow-up is often Required<T>, which uses -? to remove the modifier.

Q13: What is never used for?

The type with no values: the return type of functions that never return, and what’s left after every case of a union has been handled.

function assertNever(value: never): never {
  throw new Error(`unhandled: ${JSON.stringify(value)}`);
}

type Direction = "left" | "right" | "up";

function move(d: Direction): number {
  if (d === "left") return -1;
  if (d === "right") return 1;
  return assertNever(d);
}
console.log(move("left"));
$ npx tsc --noEmit --module nodenext q13.ts
q13.ts(10,22): error TS2345: Argument of type '"up"' is not assignable to parameter of type 'never'.

"up" wasn’t handled, so d is still "up" at the end — not never — and the call is rejected.

Q14: What is declaration merging?

Multiple declarations of the same interface (or namespace) combine into one. It’s how libraries’ types get augmented — for example, adding user to Express’s Request.

interface Animal { name: string }
interface Animal { age: number }

const rex: Animal = { name: "Rex", age: 3 };
const tom: Animal = { name: "Tom" };
console.log(rex);
$ npx tsc --noEmit --module nodenext q14.ts
q14.ts(5,7): error TS2741: Property 'age' is missing in type '{ name: string; }' but required in type 'Animal'.

Q15: What does as const do?

It infers the narrowest possible type: literal types everywhere, readonly properties, and readonly tuples instead of arrays.

export const loose = { env: "prod", port: 3000 };
export const exact = { env: "prod", port: 3000 } as const;
export const ROLES = ["admin", "user", "viewer"] as const;
export type Role = (typeof ROLES)[number];
$ npx tsc --declaration --emitDeclarationOnly --outDir types q15.ts
$ cat types/q15.d.ts
export declare const loose: {
    env: string;
    port: number;
};
export declare const exact: {
    readonly env: "prod";
    readonly port: 3000;
};
export declare const ROLES: readonly ["admin", "user", "viewer"];
export type Role = (typeof ROLES)[number];

(typeof ROLES)[number] indexes the tuple with number, producing "admin" | "user" | "viewer" — a union that stays in sync with the array.

Q16: What is the satisfies operator?

It checks a value against a type without replacing the value’s inferred type (TypeScript 4.9+).

type Colors = Record<string, string | [number, number, number]>;

const annotated: Colors = { red: [255, 0, 0], green: "#00ff00" };
const checked = { red: [255, 0, 0], green: "#00ff00" } satisfies Colors;

console.log(checked.green.toUpperCase(), checked.red[0]);
annotated.green.toUpperCase();
const invalid = { blue: [0, 0] } satisfies Colors;
$ npx tsc --noEmit --module nodenext q16.ts
q16.ts(7,17): error TS2339: Property 'toUpperCase' does not exist on type 'string | [number, number, number]'.
  Property 'toUpperCase' does not exist on type '[number, number, number]'.
q16.ts(8,19): error TS2322: Type '[number, number]' is not assignable to type 'string | [number, number, number]'.
  Type '[number, number]' is not assignable to type '[number, number, number]'.
    Source has 2 element(s) but target requires 3.

Through the annotation, green is only string | [...]; through satisfies, it’s known to be a string — and the value is still validated.

Q17: What is a conditional type, and what is distribution?

T extends U ? X : Y — a type-level ternary. When T is a bare type parameter and receives a union, the condition is applied to each member separately.

type IsString<T> = T extends string ? "yes" : "no";
type ToArray<T> = T extends unknown ? T[] : never;
type ToArrayNoDist<T> = [T] extends [unknown] ? T[] : never;

const a: IsString<"hi"> = "no";
const b: ToArray<string | number> = [1, "a"];
const c: ToArrayNoDist<string | number> = [1, "a"];
$ npx tsc --noEmit --module nodenext q17.ts
q17.ts(5,7): error TS2322: Type '"no"' is not assignable to type '"yes"'.
q17.ts(6,7): error TS2322: Type '(string | number)[]' is not assignable to type 'string[] | number[]'.
  Type '(string | number)[]' is not assignable to type 'string[]'.
    Type 'string | number' is not assignable to type 'string'.
      Type 'number' is not assignable to type 'string'.

ToArray<string | number> is string[] | number[], so a mixed array fails; wrapping in [T] stops distribution and c is accepted.

Q18: What is the difference between Exclude and Omit?

Exclude removes members from a union. Omit removes properties from an object type.

type Status = "active" | "inactive" | "banned";
interface User { id: number; name: string; password: string }

const s: Exclude<Status, "banned"> = "banned";
const u: Omit<User, "password"> = { id: 1, name: "Ada", password: "x" };
$ npx tsc --noEmit --module nodenext q18.ts
q18.ts(4,7): error TS2322: Type '"banned"' is not assignable to type '"active" | "inactive"'.
q18.ts(5,57): error TS2353: Object literal may only specify known properties, and 'password' does not exist in type 'Omit<User, "password">'.

Q19: What are covariance and contravariance?

Return types are covariant: a function returning Dog works where one returning Animal is expected. Parameters are contravariant: a function accepting any Animal works where one accepting Dog is expected — not the other way round.

interface Animal { name: string }
interface Dog extends Animal { bark(): string }

const makeDog = (): Dog => ({ name: "Rex", bark: () => "Woof" });
const handleAnimal = (a: Animal) => console.log(a.name);
const handleDog = (d: Dog) => console.log(d.bark());

const f1: () => Animal = makeDog;
const f2: (d: Dog) => void = handleAnimal;
const f3: (a: Animal) => void = handleDog;
$ npx tsc --noEmit --module nodenext q19.ts
q19.ts(10,7): error TS2322: Type '(d: Dog) => void' is not assignable to type '(a: Animal) => void'.
  Types of parameters 'd' and 'a' are incompatible.
    Property 'bark' is missing in type 'Animal' but required in type 'Dog'.

f3 would let a caller pass a cat to a function that calls bark(). (Method-syntax signatures are checked bivariantly and would let f3 through — a known soundness trade-off.)

Q20: How do you write function overloads?

List the call signatures, then one implementation that covers them all. Callers see only the overloads.

function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]): number | number[] {
  return Array.isArray(input) ? input.map(Number) : Number(input);
}

const one: number = parse("42");
const many: number[] = parse(["1", "2"]);
console.log(one + 1, many);
const bad: number = parse(["1"]);
$ npx tsc --noEmit --module nodenext q20.ts
q20.ts(10,7): error TS2322: Type 'number[]' is not assignable to type 'number'.

The return type follows the argument type, which a single union signature (number | number[]) couldn’t express.

Q21: What is the difference between Pick and Extract?

The same split as Q18: Pick keeps properties of an object type; Extract keeps members of a union.

interface User { id: number; name: string; email: string }
type Status = "active" | "inactive" | "pending";

const p: Pick<User, "name" | "email"> = { name: "Ada" };
const e: Extract<Status, "active" | "inactive" | "archived"> = "pending";
$ npx tsc --noEmit --module nodenext q21.ts
q21.ts(4,7): error TS2741: Property 'email' is missing in type '{ name: string; }' but required in type 'Pick<User, "name" | "email">'.
q21.ts(5,7): error TS2322: Type '"pending"' is not assignable to type '"active" | "inactive"'.

"archived" isn’t in Status, so Extract simply ignores it.

Q22: How do you type a class constructor, e.g. for a mixin?

new (...args: any[]) => T is the type of a constructor. Mixins take one and return a subclass.

type Constructor<T = {}> = new (...args: any[]) => T;

function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    createdAt = new Date(Date.UTC(2026, 0, 1));
  };
}

class User {
  constructor(public name: string) {}
}

const TimestampedUser = Timestamped(User);
const user = new TimestampedUser("Ada");
console.log(user.name, user.createdAt.toISOString());
$ npx tsc --noEmit --module nodenext q22.ts
$ npx tsx q22.ts
Ada 2026-01-01T00:00:00.000Z

The result type has both name from User and createdAt from the mixin.

Q23: What is NonNullable<T>, and how is it implemented?

It removes null and undefined. The built-in is T & {}; a conditional-type version works too.

type MyNonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;

const a: NonNullable<MaybeString> = null;
const b: MyNonNullable<MaybeString> = undefined;
$ npx tsc --noEmit --module nodenext q23.ts
q23.ts(4,7): error TS2322: Type 'null' is not assignable to type 'string'.
q23.ts(5,7): error TS2322: Type 'undefined' is not assignable to type 'string'.

The conditional version distributes over the union and maps null and undefined to never, which vanishes from the result.

Q24: What are template literal types?

Template literal syntax at the type level. Unions inside expand to every combination.

type Side = "top" | "right" | "bottom" | "left";
type Padding = `padding${Capitalize<Side>}`;

const ok: Padding = "paddingTop";
const bad: Padding = "paddingtop";
$ npx tsc --noEmit --module nodenext q24.ts
q24.ts(5,7): error TS2820: Type '"paddingtop"' is not assignable to type '"paddingBottom" | "paddingLeft" | "paddingRight" | "paddingTop"'. Did you mean '"paddingTop"'?

Q25: What does Awaited<T> do?

It unwraps a promise type, recursively. Awaited<ReturnType<typeof fn>> is the standard way to name what an async function resolves to.

async function getUser() {
  return { id: 1, name: "Ada" };
}

const a: Awaited<Promise<Promise<number>>> = "x";
const b: Awaited<string> = 1;
const c: Awaited<ReturnType<typeof getUser>> = { id: 1 };
$ npx tsc --noEmit --module nodenext q25.ts
q25.ts(5,7): error TS2322: Type 'string' is not assignable to type 'number'.
q25.ts(6,7): error TS2322: Type 'number' is not assignable to type 'string'.
q25.ts(7,7): error TS2741: Property 'name' is missing in type '{ id: number; }' but required in type '{ id: number; name: string; }'.

Q26: How do interface extends and type & differ on conflicts?

extends reports a conflicting property as an error at the declaration. An intersection silently produces an impossible type.

interface A { x: number }
interface B { x: string }

interface C extends A, B {}
type D = A & B;

const d: D = { x: 1 };
$ npx tsc --noEmit --module nodenext q26.ts
q26.ts(4,11): error TS2320: Interface 'C' cannot simultaneously extend types 'A' and 'B'.
  Named property 'x' of types 'A' and 'B' are not identical.
q26.ts(7,16): error TS2322: Type 'number' is not assignable to type 'never'.

D compiles on its own, and the problem only appears when you try to create one: x is number & string, which is never. That early, local error is why extends is usually preferred for composing object types.

Q27: How do you handle errors in catch blocks?

Under strict, the catch variable is unknown because JavaScript can throw anything. Narrow it, or normalise it once.

function getErrorMessage(err: unknown): string {
  if (err instanceof Error) return err.message;
  return String(err);
}

for (const thrower of [() => { throw new TypeError("bad type"); }, () => { throw "just a string"; }]) {
  try {
    thrower();
  } catch (err) {
    console.log(getErrorMessage(err));
  }
}
$ npx tsc --noEmit --module nodenext q27.ts
$ npx tsx q27.ts
bad type
just a string

Q28: What is ReturnType, and when is it useful?

It reads the return type off a function type, so a factory function can be the single source of truth for a shape.

function createUser(name: string) {
  return { id: crypto.randomUUID(), name, createdAt: new Date() };
}

type User = ReturnType<typeof createUser>;

const probe: User = { name: "Ada" };
$ npx tsc --noEmit --module nodenext q28.ts
q28.ts(7,7): error TS2739: Type '{ name: string; }' is missing the following properties from type '{ id: `${string}-${string}-${string}-${string}-${string}`; name: string; createdAt: Date; }': id, createdAt

Add a field to createUser and User gains it automatically. Note that id isn’t just string: TypeScript’s DOM library (lib.dom.d.ts) declares crypto.randomUUID() as returning a UUID-shaped template literal type.

Q29: What is Parameters<T>?

It extracts a function’s parameter list as a tuple — the tool for wrappers that must keep the original signature.

function withLogging<F extends (...args: any[]) => any>(fn: F) {
  return (...args: Parameters<F>): ReturnType<F> => {
    console.log(`${fn.name}(${args.join(", ")})`);
    return fn(...args);
  };
}

function connect(host: string, port: number) {
  return `${host}:${port}`;
}

const logged = withLogging(connect);
console.log(logged("localhost", 5432));
logged("localhost", "5432");
$ npx tsc --noEmit --module nodenext q29.ts
q29.ts(14,21): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
$ npx tsx q29.ts
connect(localhost, 5432)
localhost:5432

Q30: What is a branded type, and why use it?

Structural typing makes every number interchangeable. A brand — an intersection with a phantom property — makes, say, user IDs and post IDs incompatible, at zero runtime cost.

type UserId = number & { readonly __brand: "UserId" };
type PostId = number & { readonly __brand: "PostId" };

const userId = (n: number) => n as UserId;
const postId = (n: number) => n as PostId;

function getPost(id: PostId): string {
  return `post ${id}`;
}

console.log(getPost(postId(42)));
getPost(userId(1));
getPost(42);
$ npx tsc --noEmit --module nodenext q30.ts
q30.ts(12,9): error TS2345: Argument of type 'UserId' is not assignable to parameter of type 'PostId'.
  Type 'UserId' is not assignable to type '{ readonly __brand: "PostId"; }'.
    Types of property '__brand' are incompatible.
      Type '"UserId"' is not assignable to type '"PostId"'.
q30.ts(13,9): error TS2345: Argument of type 'number' is not assignable to parameter of type 'PostId'.
  Type 'number' is not assignable to type '{ readonly __brand: "PostId"; }'.

The brand exists only in the type system: at runtime the IDs are plain numbers, and the functions that apply the brand are the one place to put validation.

Practice

1. Without running it, predict what Result resolves to, then check with a probe: type Result = Exclude<"a" | "b" | 1 | 2, string>.

Solution
type Result = Exclude<"a" | "b" | 1 | 2, string>;
const probe: Record<Result, true> = {};
$ npx tsc --noEmit --module nodenext practice-1.ts
practice-1.ts(2,7): error TS2739: Type '{}' is missing the following properties from type 'Record<Result, true>': 2, 1

1 | 2: Exclude distributes over the union and drops every member assignable to string. A Record keyed by the union forces tsc to list the members.

2. Implement MyReadonly<T> and MyRequired<T>, and show both enforcing their rules.

Solution
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] };

interface Settings { theme?: string; fontSize?: number }

const r: MyReadonly<Settings> = { theme: "dark" };
r.theme = "light";

const full: MyRequired<Settings> = { theme: "dark" };
$ npx tsc --noEmit --module nodenext practice-2.ts
practice-2.ts(7,3): error TS2540: Cannot assign to 'theme' because it is a read-only property.
practice-2.ts(9,7): error TS2741: Property 'fontSize' is missing in type '{ theme: string; }' but required in type 'MyRequired<Settings>'.

readonly adds the modifier; -? removes the optional one. +readonly and +? also exist, as the explicit forms of adding.

3. Write type DeepReadonly<T> and show that a nested property can’t be assigned.

Solution
type DeepReadonly<T> = T extends (...args: any[]) => any ? T : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;

interface Config { db: { host: string; ports: number[] }; log: (m: string) => void }

const config: DeepReadonly<Config> = { db: { host: "localhost", ports: [5432] }, log: console.log };
config.log("still callable");
config.db.host = "remote";
config.db.ports.push(5433);
$ npx tsc --noEmit --module nodenext practice-3.ts
practice-3.ts(7,11): error TS2540: Cannot assign to 'host' because it is a read-only property.
practice-3.ts(8,17): error TS2339: Property 'push' does not exist on type 'readonly number[]'.

Functions are left alone so they stay callable; objects and arrays are made readonly at every level, so even push disappears from the nested array.

Frequently Asked Questions

What level of TypeScript knowledge do interviews test?
Most interviews cover practical knowledge: basic types, generics, utility types, and common patterns. Senior roles also test advanced type system features like conditional types, mapped types, and declaration merging.
Are TypeScript interview questions usually theoretical or coding?
Both. Expect questions about concepts (what does unknown vs any do), code reading (what does this type resolve to), and coding exercises (implement a Partial type, write a type guard).
How do I answer 'what does this type resolve to' questions?
Evaluate the type step by step the way the compiler does: expand aliases, distribute conditional types over unions, and drop never members. To check yourself while practising, assign a deliberately wrong value to the type and read the tsc error, which prints the resolved type.
Which TypeScript version should I assume in an interview?
Assume a recent version with strict mode on, since that is the default for new projects and the default in TypeScript 6. Mention version-dependent behaviour when it matters, such as satisfies arriving in 4.9 or unknown catch variables in 4.4.