TypeScript Coding Interview Gotchas: What Compiles and Still Crashes

- Type assertions (
as T) are erased at runtime — TypeScript compiles them away entirely, leaving no safety net if you're wrong - Non-null assertion (
!) produces zero runtime code — if the value is absent you get a TypeError with no TypeScript warning anysilences the type checker;unknownforces narrowing before use — preferunknownfor any arbitrary or external input- Numeric enums compile to a bidirectional JS object —
Object.keys()returns twice the entries you expect, silently breaking loops readonlyis compile-time only — runtime immutability requiresObject.freeze(), which TypeScript also narrows correctlyvoidin callback position means "ignore the return value," not "return nothing" — the two forms look identical and behave differently- Use
??not||in DP tables —||overwrites valid zero cells;??only replacesnullandundefined
TypeScript promises to save you from yourself. The compiler checks your types, catches your typos, and red-squiggles your bad ideas. Then it compiles to JavaScript and throws every single type away. No types exist at runtime. Zero. That one fact explains most of the gotchas in this guide, so burn it into your brain before you write a single interview line in TypeScript.
For Map operations, built-in collections, and standard library basics, the TypeScript interview cheat sheet covers the vocabulary. This piece is about what goes wrong after you've learned the vocabulary.
as Is a Pinky Promise to the Compiler
Type assertions do not check anything. as T tells the compiler "I know better than you." The compiler says "fair enough, you're the boss" and moves on. The assertion disappears at runtime. If you were wrong, you find out the worst possible way:
const parsed = JSON.parse('{"count": "five"}') as { count: number }; console.log(parsed.count + 1); // "five1", no error, no warning
The double assertion (as unknown as T) is worse because it also suppresses the "these types don't overlap" safety check, which was the last thing standing between you and chaos:
const n = "hello" as unknown as number; console.log(n.toFixed(2)); // TypeError at runtime
Type assertions shift responsibility from the compiler to you, with zero runtime backup. For code that parses external data or processes interview input, use a type guard instead:
function isMatrix(val: unknown): val is number[][] { return Array.isArray(val) && val.every(row => Array.isArray(row)); }
! Is an Ejector Seat
The non-null assertion operator tells TypeScript a value is definitely not null or undefined. It compiles to nothing. Literally nothing. Not a check. Not a fallback. The output JavaScript has no memory this operator ever existed:
const first = arr[0]!; // "trust me it exists" first.toString(); // TypeError if arr is empty (no TS error, full crash)
In interview solutions you sometimes see this used to skip if checks on array accesses. That is exactly the wrong place to use it. The check is the correctness guarantee:
// bad: lies about what can happen function head(arr: number[]): number { return arr[0]!; } // correct: the return type tells the truth function head(arr: number[]): number | undefined { return arr[0]; }
Use ! only when you have information the type system cannot express and you are genuinely certain the value is present. In a 45-minute interview, "genuinely certain" is a high bar.

TypeScript: "I don't see any issues." Runtime: same energy.
any Opens a Hole. unknown Closes It.
any turns off the type checker for that value entirely. You can call .xyz() on an any and TypeScript will not complain. It is TypeScript with its eyes closed, humming contentedly:
function process(val: any) { val.toUpperCase(); // no error, will crash if val is a number }
unknown is the safe counterpart. TypeScript forces you to narrow before doing anything:
function processSafe(val: unknown) { val.toUpperCase(); // Error: Object is of type 'unknown' if (typeof val === "string") { val.toUpperCase(); // fine, TypeScript narrowed it } }
When a function accepts arbitrary input, use unknown. When you know what it is, narrow it. Use any only when crossing a boundary you control and have verified. If you are coming from JavaScript and wondering why the TypeScript version still crashes, check the JavaScript for coding interviews guide for the underlying JS behaviors that TypeScript does not prevent.
Numeric Enums Have a Hidden Second Direction
Numeric enums compile to a bidirectional JavaScript object. Both directions. You probably only knew about one:
enum Direction { Up, Down, Left, Right } Direction.Up // 0 Direction[0] // "Up" (reverse mapping) Object.keys(Direction) // ["0", "1", "2", "3", "Up", "Down", "Left", "Right"] eight entries, not four
Iterating Object.keys() or Object.values() on a numeric enum gives you twice the entries you expect. This silently breaks loops, Set construction from enum keys, and any interview solution that iterates over enum members. The compiler is delighted. Your output is garbage.
String enums do not have reverse mapping, so Object.keys() works normally. const enum goes the other direction: the entire enum is erased at compile time and each use gets the literal value inlined. You cannot iterate it at runtime at all:
const enum Status { Active = "active", Inactive = "inactive" } // After compilation: no object exists. Status.Active becomes "active" everywhere. Object.keys(Status); // runtime error: Status is not defined
For most interview solutions, as const objects sidestep all of this:
const Direction = { Up: 0, Down: 1, Left: 2, Right: 3 } as const; type Direction = typeof Direction[keyof typeof Direction]; // 0 | 1 | 2 | 3 Object.keys(Direction); // ["Up", "Down", "Left", "Right"] no surprises
Excess Property Checks Only Fire at the Exact Point of Assignment
TypeScript checks for extra properties on object literals. But only when you assign the literal directly. Route through a variable and the check quietly disappears:
type Point = { x: number; y: number }; // Error: Object literal may only specify known properties const p1: Point = { x: 1, y: 2, z: 3 }; // No error. Structural typing applies, z is ignored. const temp = { x: 1, y: 2, z: 3 }; const p2: Point = temp;
Same for function calls: object literal arguments get checked, variable arguments do not.
This means TypeScript will happily let extra fields slip through whenever you route through a variable. If you expect TypeScript to catch a typo in a property name and it does not, check whether you are going through a variable rather than a literal.
readonly Doesn't Freeze Anything at Runtime
readonly is compile-time only. The JavaScript output contains no immutability enforcement whatsoever:
type Config = { readonly timeout: number }; const cfg: Config = { timeout: 3000 }; cfg.timeout = 5000; // TypeScript error at compile time (cfg as any).timeout = 5000; // runtime: works fine, no error
ReadonlyArray<T> prevents push, pop, and splice at the type level, but the underlying array is fully, happily mutable:
const nums: ReadonlyArray<number> = [1, 2, 3]; nums.push(4); // TypeScript error (nums as number[]).push(4); // runtime: succeeds silently
For actual immutability you need Object.freeze(). TypeScript narrows the returned type to Readonly<T> automatically, so you get both the type safety and the runtime guarantee. No pinky promises required.
void in a Callback Type Is Not What You Think
A function typed as () => void can return any value. That is intentional:
type Callback = () => void; const fn: Callback = () => 42; // TypeScript: fine
It exists so that [1, 2, 3].forEach(console.log) compiles even though console.log returns undefined and forEach expects (item: T) => void.
The gotcha is that void in an explicit return type is stricter:
function explicit(): void { return 42; // Error: Type 'number' is not assignable to type 'void' }
In callback type positions, void means "I do not care what this returns." In explicit function return types, void means "this must return nothing." These look identical and behave differently. This is not a bug. The TypeScript designers knew exactly what they were doing. You just have to know too.
typeof Cannot Tell Arrays from Objects
Inherited directly from JavaScript, and TypeScript's type narrowing makes it easy to forget:
typeof [] // "object" typeof {} // "object" typeof null // "object"
Narrowing with typeof val === "object" matches all three. To distinguish them you need separate checks:
function handle(val: string | number[] | null) { if (typeof val === "string") { /* string */ } if (Array.isArray(val)) { /* number[] */ } if (val === null) { /* null */ } }
Discriminated unions are the cleaner solution for interview problems: add a kind field to each variant and TypeScript narrows on that without any typeof juggling. See the pattern recognition guides on BFS and DFS for cases where discriminated unions help with graph node types.
|| vs ?? in DP Tables
You know this from JavaScript but TypeScript makes the type leak visible:
function timeout(ms: number | undefined): number { return ms || 5000; // 0 becomes 5000, wrong for zero-millisecond timeouts return ms ?? 5000; // 0 passes through, only undefined triggers fallback }
|| replaces any falsy value. ?? only replaces null and undefined.
In dynamic programming tables this matters constantly. dp[i] || defaultValue overwrites valid 0 cells, corrupting your solution silently. dp[i] ?? defaultValue is safe. Same problem with dp[i][j] || Infinity in shortest-path tables: if the real cost is zero, you just replaced it with infinity and the algorithm runs happily to the wrong answer.
For more on when this and other type behaviors affect language choice, see the JavaScript vs TypeScript comparison.
TypeScript Interview Gotchas: Quick Reference
| Gotcha | Symptom | Fix |
|---|---|---|
value as T | Runtime crash despite compiling | Type guard (instanceof, typeof, custom) |
value! | TypeError on null or undefined | Explicit null check |
any everywhere | Type checker fully silenced | unknown + narrowing |
Numeric enum Object.keys() | Double the expected entries | as const object |
| Extra property on a variable | Silently passes type check | Test with direct object literal |
readonly mutated via as any | No TypeScript error | Object.freeze() |
() => void callback returns value | Unexpected behavior downstream | Explicit () => undefined |
typeof val === "object" | Matches null, arrays, objects | Array.isArray() and === null |
dp[i] || default | Valid zero cells overwritten | dp[i] ?? default |
All of this is table knowledge until someone asks you to explain it out loud. SpaceComplexity runs voice-based mock interviews where you work through TypeScript-specific gotchas in real time, the same way an interviewer would probe them after your solution compiles. Knowing the trap is one thing. Explaining why as compiles but crashes while someone watches is the skill that closes offers.