Most TypeScript interview prep online is a wall of syntax. Memorize Pick, memorize Partial, recite the difference between type and interface, and hope the interviewer doesn't ask anything that actually matters.
The questions seniors get asked are different. They're less "can you use a mapped type" and more "how do you model a payment state machine so invalid combinations don't compile". Less syntax, more judgement.
This guide is the version I'd want to read the night before an interview: the mental model, the senior-level talking points, the pitfalls that catch experienced engineers, and the answers that signal you've actually shipped TypeScript in production.
The Mental Model: Types Disappear At Runtime
If you take one thing into the interview, take this:
TypeScript is a compile-time tool. At runtime, your code is just JavaScript.
type User = {
id: string;
name: string;
};
async function getUser(): Promise<User> {
const response = await fetch("/api/user");
return response.json(); // ← lying. This is `any` pretending to be User.
}
The Promise<User> annotation tells your editor and the rest of your codebase what to expect. It does nothing to validate the response. If the API returns { id: 1, name: null }, TypeScript will not catch it.

Senior answer:
TypeScript protects internal code contracts. External boundaries like API responses, form input, env vars, webhook payloads, database results, and third-party SDKs still need runtime validation. I treat unknown data as
unknownand parse it before trusting it.
This single idea unlocks half the senior-level questions you'll get.
type vs interface
Both describe object shapes. The differences:
// interface supports declaration merging
interface User {
id: string;
}
interface User {
name: string;
}
// User is now { id: string; name: string }
// type is more flexible for unions, intersections, mapped types,
// conditional types, template literals, and primitives
type Status = "idle" | "loading" | "success" | "error";
type Nullable<T> = T | null;
Senior answer:
I use
interfacefor public object contracts that might be extended (especially when authoring libraries or augmenting external types), andtypefor unions, utility compositions, mapped types, and anything that isn't a plain object shape. In application code, the choice matters less than people pretend.
Module augmentation (the real reason interface merging matters)
Declaration merging isn't just a curiosity: it's how you extend types from third-party libraries you don't own:
import "express";
declare module "express" {
interface Request {
user?: User;
requestId: string;
}
}
After that file is loaded, req.user is typed correctly across your entire codebase. Same pattern works for process.env, Window, Vite's ImportMeta, and anywhere a library exposes a mergeable interface.
Senior answer:
I reach for module augmentation when I need to add fields to a third-party type, typically Express's
Request,process.env, or Vite'sImportMetaEnv. It's one of the legitimate reasons I preferinterfaceovertypefor public contracts.
any, unknown, and never
This trio is one of the most common interview topics and one of the easiest places to sound senior.
any disables type checking
let value: any = "hello";
value.toUpperCase();
value.thisDoesNotExist();
value.foo.bar.baz; // all "fine"
any is contagious: it spreads through your code and silently kills type safety wherever it lands.
unknown is any with a seatbelt
let value: unknown = "hello";
value.toUpperCase(); // ❌ Error
if (typeof value === "string") {
value.toUpperCase(); // ✅ narrowed
}
You must narrow before using it. That's the point.
never represents impossible values
The classic use case is exhaustiveness checking:
type PaymentStatus =
| { type: "pending" }
| { type: "paid"; paymentId: string }
| { type: "failed"; reason: string };
function handleStatus(status: PaymentStatus): string {
switch (status.type) {
case "pending":
return "Payment is pending";
case "paid":
return `Paid: ${status.paymentId}`;
case "failed":
return `Failed: ${status.reason}`;
default: {
const _exhaustive: never = status;
throw new Error(`Unhandled status: ${JSON.stringify(_exhaustive)}`);
}
}
}
If someone adds | { type: "refunded" } later, the compiler will reject the default block until you handle the new case. That throw matters. Returning silently in production is how impossible states ship.

Senior answer:
I use
unknownfor any data crossing a boundary into my code, andneverto make impossible cases visible at compile time. Exhaustive switches over discriminated unions are one of the highest-value patterns in TypeScript: they turn a future bug into a compile error.
Discriminated Unions: The Pattern That Matters Most
This is probably the single most important pattern to internalize for a senior interview.
The bad version
type RequestState<T> = {
loading: boolean;
error?: string;
data?: T;
};
This allows nonsense:
{ loading: true, error: "Failed", data: user }
Loading and failed and successful, all at once. This kind of state shape is the cause of more frontend bugs than any other single thing.
The good version
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
function render(state: RequestState<User>): string {
switch (state.status) {
case "idle": return "Nothing yet";
case "loading": return "Loading…";
case "success": return state.data.name; // data is guaranteed
case "error": return state.error; // error is guaranteed
}
}
Invalid combinations don't compile. The narrowing inside each case is automatic.

Senior answer:
Discriminated unions let you make invalid states unrepresentable. That's the senior-level mindset shift: you're not annotating data, you're modeling a domain. If a combination of fields can't legally exist, the type system shouldn't allow it to be expressed.
Generics That Don't Suck
Generics confuse people because they look like magic. They're not: they're parameters for types.
Basic generics
function identity<T>(value: T): T {
return value;
}
T is a placeholder. Whatever you pass in, that's what comes out.
Constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Nazar" };
const name = getProperty(user, "name"); // string
const id = getProperty(user, "id"); // number
const bad = getProperty(user, "wrongKey"); // ❌ Error
K extends keyof T says: K must be a valid key of T. T[K] says: the return type is whatever that property's type is.
This is the senior move, preserving the relationship between input and output, instead of returning any.
![Three-step horizontal flow diagram titled "How getProperty<T, K extends keyof T>(obj: T, key: K): T[K] Resolves" walking through inputs, type inference, and output, with a compile-error example.](/assets/imgs/articles/typescript-interview-prep/how-getproperty-resolves.png)
Generic React component
type SelectProps<T> = {
items: T[];
getLabel: (item: T) => string;
onSelect: (item: T) => void;
};
export function Select<T>({ items, getLabel, onSelect }: SelectProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={i} onClick={() => onSelect(item)}>
{getLabel(item)}
</li>
))}
</ul>
);
}
The component works for any item shape, and onSelect knows the exact type.
NoInfer (TS 5.4+)
A modern feature worth knowing:
function createState<T>(initial: T, fallback: NoInfer<T>): T {
return Math.random() > 0.5 ? initial : fallback;
}
createState("hello", "world"); // T inferred from `initial`, fallback must match
Without NoInfer, both arguments would contribute to inferring T, sometimes in ways you don't want.
const type parameters (TS 5.0+)
function asTuple<const T extends readonly unknown[]>(arr: T): T {
return arr;
}
const t = asTuple(["a", "b", "c"]);
// inferred as readonly ["a", "b", "c"], not string[]
Senior answer:
Generics aren't about clever abstractions. They're about preserving type relationships across boundaries, from input to output, from props to callbacks, from query to result. The win is that consumers get accurate types without me having to write them by hand.
Utility Types You Should Know Cold
| Utility | What it does |
|---|---|
Partial<T> |
Makes all properties optional |
Required<T> |
Makes all properties required |
Readonly<T> |
Makes all properties readonly |
Pick<T, K> |
Selects properties K from T |
Omit<T, K> |
Removes properties K from T |
Record<K, V> |
Object with keys K and values V |
ReturnType<F> |
Return type of function F |
Parameters<F> |
Parameter tuple of function F |
Awaited<T> |
Unwraps Promise<T> to T |
NonNullable<T> |
Removes null and undefined |
Extract<T, U> |
Members of T assignable to U |
Exclude<T, U> |
Members of T not assignable to U |
Two patterns worth memorizing:
// Derive an input type from a domain type
type CreateUserInput = Omit<User, "id" | "createdAt">;
// Derive a service return type without redeclaring it
type FetchUser = Awaited<ReturnType<typeof fetchUser>>;

Advanced Type Mechanics
Mapped types
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Concrete<T> = { [K in keyof T]-?: T[K] };
type Nullable<T> = { [K in keyof T]: T[K] | null };
The -? and -readonly syntax removes modifiers. The ? and readonly syntax adds them.

Conditional types and infer
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;
type ArrayItem<T> = T extends readonly (infer Item)[] ? Item : never;
type A = UnwrapPromise<Promise<string>>; // string
type B = ArrayItem<{ id: number }[]>; // { id: number }
infer extracts a type from another type's structure. This is how libraries derive things like "the params of this React Query hook" or "the response type of this tRPC procedure."
Distributive conditional types (the senior-level gotcha)
When a conditional type's checked type is a naked type parameter, it distributes over unions:
type ToArray<T> = T extends unknown ? T[] : never;
type A = ToArray<string | number>;
// distributes: ToArray<string> | ToArray<number>
// = string[] | number[] ← probably not what you wanted
To prevent distribution, wrap in a tuple:
type ToArrayBoxed<T> = [T] extends [unknown] ? T[] : never;
type B = ToArrayBoxed<string | number>;
// = (string | number)[]
This is also how NonNullable<T> and Exclude<T, U> work internally: they rely on distribution to filter union members.
Senior answer:
Distributive conditional types are great when you want to map across a union, and a footgun when you don't. The fix is the
[T] extends [U]tuple wrap: that prevents distribution and treats the union as a single type.
Template literal types
type Entity = "user" | "product" | "order";
type Action = "created" | "updated" | "deleted";
type EventName = `${Entity}.${Action}`;
// "user.created" | "user.updated" | ... | "order.deleted"
Used heavily for event systems, route names, CSS class generators, and type-safe SQL builders.
as const and satisfies (the modern combo)
const ROUTES = {
home: "/",
users: "/users",
settings: "/settings",
} as const satisfies Record<string, `/${string}`>;
type RouteName = keyof typeof ROUTES;
// "home" | "users" | "settings"
type RoutePath = (typeof ROUTES)[RouteName];
// "/" | "/users" | "/settings"
satisfies validates the shape against a contract without widening the type. Compare with the old way:
const ROUTES: Record<string, string> = {
home: "/",
users: "/users",
};
ROUTES.home; // string — literal type lost
Senior answer:
satisfiesis one of the most underrated features added to TypeScript. It lets me validate config shapes, route maps, theme objects, and event registries against a contract while keeping the precise inferred types for downstream usage.
Runtime Boundaries: Where TypeScript Ends
This is the topic that separates juniors from seniors.

Type guards
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
typeof (value as Record<string, unknown>).id === "string" &&
typeof (value as Record<string, unknown>).name === "string"
);
}
Hand-rolled guards work, but they're tedious and easy to get wrong.
Assertion functions (asserts)
A close cousin of type guards. Instead of returning true/false, an assertion function throws and TypeScript narrows everything after the call:
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new Error("Expected User");
}
}
function processUser(value: unknown) {
assertIsUser(value);
// value is User from this line on, no `if` block needed
console.log(value.name);
}
Useful when you'd rather throw than branch. Schema parsers like Zod's .parse() are essentially assertion functions under the hood.
The filter type predicate pattern
This shows up in production code constantly, and it's one of the cleanest ways to demonstrate fluency:
const items: (User | null)[] = [user1, null, user2];
// ❌ Doesn't narrow — still (User | null)[]
const bad = items.filter((x) => x !== null);
// ✅ Type predicate narrows to User[]
const good = items.filter((x): x is User => x !== null);
The (x): x is User return-type annotation is what tells TypeScript the filter actually narrows the array.
unknown in catch blocks
With useUnknownInCatchVariables (or strict), caught errors are typed as unknown, not any:
try {
await doThing();
} catch (err) {
// err is unknown — must narrow
if (err instanceof Error) {
console.log(err.message);
} else {
console.log("Unknown error", err);
}
}
This catches the very common bug of assuming everything thrown is an Error. In JavaScript, anything can be thrown: strings, numbers, even undefined.
Validation libraries (the modern answer)
In production codebases the answer to "how do you type API responses" is almost always Zod, Valibot, or ArkType:
import { z } from "zod";
export const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(["admin", "user", "guest"]),
});
export type User = z.infer<typeof UserSchema>;
export async function getUser(): Promise<User> {
const res = await fetch("/api/user");
const json: unknown = await res.json();
return UserSchema.parse(json); // throws on invalid data
}
The pattern to internalize: the schema is the source of truth, and the type is derived from it. One source, no drift between runtime check and type.

Senior answer:
I derive types from runtime schemas, not the other way around. With
z.infer<typeof Schema>, the validation logic and the static type are guaranteed to stay in sync. I treat every external boundary (API, form, env, queue, third-party) asunknownuntil parsed.
Function Overloads vs Generics
Sometimes the input/output relationship needs more than a single signature. Two tools:
Overloads: different shapes for different inputs
function parseValue(value: string): string;
function parseValue(value: number): number;
function parseValue(value: string | number): string | number {
return typeof value === "string" ? value.trim() : value * 2;
}
Generics: when one shape preserves through
function wrap<T>(value: T): T[] {
return [value];
}
Senior answer:
I reach for overloads when the input/output relationship genuinely changes shape, for example by returning different types based on a flag argument. For everything else, generics are cleaner. Overloads are easy to misuse and hard to read once you have more than two of them.
tsconfig.json for Senior Engineers
Knowing the strictness flags signals experience:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true,
"useUnknownInCatchVariables": true
}
}
The two most impactful beyond strict:
noUncheckedIndexedAccess: array and record access returns T | undefined:
const users: string[] = ["Nazar"];
const first = users[0]; // string | undefined, not string
This catches more real bugs than almost any other flag.
exactOptionalPropertyTypes: name?: string means "may be missing", not "may be undefined":
type User = { name?: string };
const u: User = { name: undefined }; // ❌ with exact, ✅ without
Senior answer:
I turn on
strictfrom day one. Adding it to a large existing codebase is painful, so I'd rather pay the cost upfront.noUncheckedIndexedAccessin particular is non-negotiable for me: it surfaces a class of bugs that otherwise only show up in production.
Senior-Level Topics You Should Be Ready For
Structural vs nominal typing
TypeScript is structural: types are compatible if their shapes are compatible, regardless of name.
type User = { id: string; name: string };
type Employee = { id: string; name: string };
const u: User = {} as Employee; // ✅ — shapes match
Compare to Java/C#, where two classes with identical fields are still incompatible.
Branded (nominal) types
When you want nominal-style safety in a structural system:
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function getUser(id: UserId) { /* … */ }
function getOrder(id: OrderId) { /* … */ }
const uid = "abc" as UserId;
const oid = "xyz" as OrderId;
getUser(oid); // ❌ Error — different brand
Useful for IDs, sanitized strings, currency amounts in different units, and anything where mixing two values of the same primitive type would be a bug.
enum vs union of literals
Most senior codebases avoid enum in favor of unions:
// Avoid
enum Role { Admin, User, Guest }
// Prefer
type Role = "admin" | "user" | "guest";
Reasons: numeric enums have weird reverse-mapping behavior, const enum interacts badly with isolatedModules, and unions compose better with template literals and mapped types. enum does emit runtime code; unions don't.
Variance (covariance and contravariance)
Worth understanding even if you don't get asked directly:
- Covariance: a
Dog[]is assignable toAnimal[](function return positions) - Contravariance: a function taking
Animalis assignable where a function takingDogis expected (function parameter positions)
The classic gotcha:
type Handler<T> = (event: T) => void;
const handleAnimal: Handler<{ kind: string }> = (e) => console.log(e.kind);
const handleDog: Handler<{ kind: string; breed: string }> = handleAnimal;
// ✅ allowed — but if handleDog is called with a Dog, the body might
// access .breed and the assigned handler doesn't have it.
TypeScript treats function parameters as bivariant by default for ergonomics, which is technically unsound. The strictFunctionTypes flag (on with strict) enforces contravariance for function-type expressions but not method syntax: a footgun worth knowing about.

Recursive types
type Json =
| string
| number
| boolean
| null
| Json[]
| { [key: string]: Json };
type TreeNode<T> = {
value: T;
children: TreeNode<T>[];
};
Type-only imports
import type { User } from "./types";
import { type Logger, createLogger } from "./logger";
These are stripped at compile time: they don't generate runtime imports. Important for tree-shaking, circular-dependency avoidance, and isolatedModules compatibility (which bundlers like esbuild and SWC require).
Common Pitfalls Interviewers Like To Probe
Object.keys returns string[], not keyof T:
const user = { id: "1", name: "Nazar" };
Object.keys(user).forEach((k) => {
user[k]; // ❌ Error — k is string, not keyof typeof user
});
This is intentional: TypeScript can't prove the object doesn't have extra keys at runtime due to structural typing.
The void return type footgun:
type Callback = () => void;
const cb: Callback = () => 42; // ✅ allowed!
void in a callback type means "I don't care what you return", not "you must return nothing." This trips people up with Array.prototype.forEach and similar APIs.
Excess property checks only fire on object literals:
type User = { id: string; name: string };
const direct: User = { id: "1", name: "Nazar", age: 30 }; // ❌
const raw = { id: "1", name: "Nazar", age: 30 };
const indirect: User = raw; // ✅
Empty interface vs Record<string, never>:
interface Empty {} // assignable from almost anything
type EmptyObj = Record<string, never>; // truly empty
@ts-expect-error vs @ts-ignore:
// @ts-ignore — silences forever, even if the underlying error goes away
// @ts-expect-error — silences now, but errors if the underlying error is fixed
Always prefer @ts-expect-error: it self-cleans when the issue is resolved, so dead suppressions don't accumulate.
Practice Exercises
Three exercises that show up in some form in most senior interviews.
1. Type-safe pick
function pick<T extends object, K extends keyof T>(
obj: T,
keys: readonly K[],
): Pick<T, K> {
const result = {} as Pick<T, K>;
for (const key of keys) result[key] = obj[key];
return result;
}
2. Type-safe Result
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function unwrap<T, E>(r: Result<T, E>): T {
if (r.ok) return r.value;
throw r.error;
}
3. Type-safe event emitter
type EventMap = {
"user.created": { id: string; name: string };
"user.deleted": { id: string };
"order.paid": { orderId: string; amount: number };
};
class TypedEmitter<E extends Record<string, unknown>> {
private listeners: { [K in keyof E]?: Array<(payload: E[K]) => void> } = {};
on<K extends keyof E>(event: K, fn: (payload: E[K]) => void): void {
(this.listeners[event] ??= []).push(fn);
}
emit<K extends keyof E>(event: K, payload: E[K]): void {
this.listeners[event]?.forEach((fn) => fn(payload));
}
}
const bus = new TypedEmitter<EventMap>();
bus.on("user.created", (p) => p.name); // p is fully typed
bus.emit("order.paid", { orderId: "1", amount: 99 });
Common Interview Questions With Strong Answers
1. What is TypeScript?
TypeScript is a statically typed superset of JavaScript that adds compile-time type checking. It improves tooling, refactoring safety, and acts as living documentation. It doesn't change runtime behavior. Types are erased during compilation.
2. Does TypeScript guarantee runtime safety?
No. Types are erased at compile time. External data like API responses, form input, env vars, and third-party SDKs still needs runtime validation, typically with libraries like Zod.
3. any vs unknown?
anydisables type checking and is contagious.unknownrequires narrowing before use. I default tounknownfor untrusted data and only fall back toanywhen interoperating with poorly-typed third-party code, with a comment explaining why.
4. type vs interface?
Both describe object shapes.
interfacesupports declaration merging, which makes it useful for public contracts and module augmentation (extending Express'sRequest, for example).typeis more flexible: unions, intersections, mapped and conditional types, primitives. In application code I lean ontype; in library code I preferinterfacefor extendable contracts.
5. What's a discriminated union and why use it?
A union where each member shares a common literal property used for narrowing. They let me model state machines so that invalid combinations are unrepresentable. The canonical example is replacing
{ loading, error?, data? }with separateidle,loading,success, anderrorvariants.
6. What is never and when do you use it?
neverrepresents impossible values. Most often I use it for exhaustive checks in switch statements over discriminated unions: assigning the discriminant toneverin the default case turns "we forgot to handle a case" into a compile error.
7. What are generics for?
Preserving type relationships across boundaries. Instead of returning
anyand losing type information, generics let me say "the output's type depends on the input's type", as inpick<T, K extends keyof T>returningPick<T, K>.
8. What's satisfies and when do you reach for it?
satisfiesvalidates a value against a type without widening to that type. Useful for config objects, route maps, theme tokens, and anywhere I want to enforce a contract while keeping precise literal types for inference downstream.
9. How do you type API responses safely?
I treat the response as
unknown, parse it through a runtime schema (Zod, Valibot, or ArkType), and derive the static type from the schema withz.infer. That way the type and the validation logic can't drift.
10. What's structural typing?
TypeScript compares types by shape, not by name. Two types with the same fields are compatible regardless of declaration. When I need nominal-style guarantees (for IDs, units, sanitized strings), I use branded types.
11. What's a distributive conditional type?
When a conditional type's checked type is a naked type parameter, it distributes over unions. So
T extends U ? X : Yapplied toA | Bbecomes(A extends U ? X : Y) | (B extends U ? X : Y). To prevent distribution, wrap in a tuple:[T] extends [U]. This is howNonNullableandExcludework internally.
Pro Tips
- Let inference do the work locally; annotate at boundaries. Don't write
const x: number = 1. Do annotate exported function signatures and public APIs. - Prefer
unknowntoany, always. Treatanyas a code smell that needs justification. - Derive types from runtime schemas, not the reverse.
z.infer<typeof Schema>keeps validation and types in sync. - Make invalid states unrepresentable. Reach for discriminated unions before defensive checks.
- Avoid type assertions (
as Foo) when possible. Use type guards or schema parsing instead. Eachasis a small lie to the compiler. - Don't over-engineer types. A four-line type is usually better than a clever one-liner. Library code is the place for type wizardry; application code is the place for clarity.
- Turn on
noUncheckedIndexedAccessearly. It catches a real class of bugs that nothing else does. - Prefer
@ts-expect-errorover@ts-ignore. It self-cleans when the underlying issue is fixed. - Know which utility types exist so you don't reinvent
Pick,Omit,ReturnType, orAwaited.
Study Plan (Flexible: 3 to 10 Days)
Use whichever pacing fits your timeline. The phases are the same. Only the depth and time per phase changes. The goal is the same too: walk in confident, with concrete examples ready.
Phase 1: Foundations (the non-negotiables)
- The mental model: types disappear at runtime
typevsinterface(and module augmentation)any/unknown/never- Union, intersection, narrowing
- Discriminated unions (the most important pattern)
keyof,typeof, indexed access types
3-day pace: half a day. 10-day pace: day 1, with hands-on examples for each concept.
Phase 2: Generics & Utility Types
- Generic functions, generic React components
- Constraints (
K extends keyof T) - All twelve core utility types
NoInfer,consttype parameters
3-day pace: half a day. 10-day pace: days 2-3, including writing your own versions of
Partial,Pick,Omit, andRecordfrom scratch.
Phase 3: Advanced Type Mechanics
- Mapped types and modifiers
- Conditional types and
infer - Distributive conditional types
- Template literal types
as constandsatisfies
3-day pace: day 2. 10-day pace: days 4-5, with one small exercise per topic.
Phase 4: Runtime Boundaries
- Type guards, assertion functions, the
filterpredicate pattern unknownincatch- Zod / Valibot / ArkType,
z.infer<typeof Schema> - Designing the API/form/env/webhook validation layer
3-day pace: half a day. 10-day pace: days 6-7, with a small project where you replace
ascasts with schema parsing.
Phase 5: Senior Topics
- Structural vs nominal typing, branded types
- Variance (covariance / contravariance / bivariance gotcha)
- Recursive types, type-only imports
enumvs union literals- Function overloads vs generics
3-day pace: half a day. 10-day pace: days 8-9.
Phase 6: Mock Interview & Project Stories
Walk through your real projects with these prompts in mind:
- Where did TypeScript catch a bug for you?
- Where did you use generics to preserve types across a layer?
- Where did you add runtime validation? What library, and why?
- Where did you reach for a discriminated union? What did it replace?
- What strict flags do you turn on, and why?
Practice the Common Interview Questions answers above out loud, twice. Once dry, once with a concrete example from your work attached.
3-day pace: day 3. 10-day pace: day 10.
Final Thoughts
The senior-level signal in a TypeScript interview isn't reciting Pick<T, K>. It's showing that you understand why the type system exists, where it ends, and how to design types that prevent bugs instead of just describing data.
The mindset shift, in one sentence:
You're not annotating code. You're modeling a domain so that wrong code doesn't compile.
Bring that energy in, back it with concrete examples from your own work, and the rest is just remembering syntax. Go ace it 👊






