TypeScript Interview — Unknown vs Any Production Gotcha
TypeScript's 'unknown' type forces type guards, avoiding runtime crashes.
20+ years shipping production code across the stack, with years spent interviewing engineers. Lessons pulled from things that broke in production.
anydisables all type checking — it's a backdoor to plain JavaScript. Use it only for migration or third-party gaps.unknownforces a type guard before use — the safe container for API responses, user input, or deserialized JSON.neverrepresents values that can never occur — used in exhaustive switch checks and functions that always throw.- Production rule default to
unknownfor external data; treatanyas technical debt that must be repaid with a type guard.
Imagine you're running a restaurant kitchen. JavaScript is like having cooks who can grab ANY ingredient from ANY shelf with zero labels — fast, but chaotic. TypeScript is like labelling every container: 'this shelf is ONLY for spices, this one ONLY for sauces.' The kitchen still runs the same way at service time (it still compiles to JavaScript), but during prep you catch mistakes before a dish goes out wrong. That's TypeScript — a safety net that lives at development time, not runtime.
TypeScript has gone from a Microsoft experiment to the de facto standard for serious JavaScript projects. React, Angular, Node, and NestJS teams all default to it now — not because it's trendy, but because it catches entire categories of bugs before your code ships. If you're interviewing at any company with a mature frontend or full-stack team, TypeScript fluency isn't optional anymore.
The problem TypeScript solves is subtle but expensive: JavaScript's dynamic typing is powerful but it lets you pass a string where a number was expected, access a property that doesn't exist, or call undefined as a function — all silently, until your user hits the bug in production. TypeScript adds a compile-time type checker that acts like a code reviewer who never sleeps and never misses a thing.
By the end of this article you'll be able to explain the difference between type and interface, use generics to write reusable code without sacrificing type safety, reason about unknown vs any, and walk into an interview ready to answer follow-up questions that trip up even experienced developers.
Why 'What Is TypeScript Interview Questions?' Misses the Point
TypeScript interview questions are not trivia—they are probes into your understanding of the type system's sharp edges. The core mechanic is that TypeScript adds a static type layer on top of JavaScript, catching type mismatches at compile time rather than runtime. But the real test is how you handle the escape hatches: any and unknown. These two types define the boundary between type safety and chaos. any disables all type checking—it's a backdoor to plain JavaScript. unknown forces a type check before use—it's the safe container for values you don't know yet. In practice, unknown is the correct choice for API responses, user input, or deserialized JSON. any should be reserved for migration pain or third-party library gaps. Teams that default to any lose the entire benefit of TypeScript: they ship bugs that the compiler could have caught. The production insight is simple: any propagates silently; unknown forces you to prove safety at every access point. Senior engineers treat unknown as the default for external data and any as a debt that must be repaid with a type guard.
any to a variable poisons every downstream usage—TypeScript stops checking that entire branch. unknown forces a cast or type guard before use.any for API responses lose compile-time validation; a renamed field in the backend silently becomes undefined in the frontend.unknown, then validate with a type guard or Zod schema before treating it as your domain type.unknown is the safe default for any value whose type you don't control at compile time.any is a type-system escape hatch that should be treated as technical debt with a clear removal plan.unknown and your application types.type vs interface — Which One Do You Actually Reach For?
This is the most common TypeScript interview question, and most candidates answer it wrong — not because they don't know the syntax, but because they can't explain the practical difference.
Both type and interface describe the shape of an object. They're interchangeable in most everyday cases, which is exactly why interviewers ask the question — they want to know if you understand the edges.
The critical difference is that interfaces are open — you can declare the same interface in multiple places and TypeScript merges them. This is called declaration merging. Libraries use this heavily: they ship a type definition and let you extend it in your own code without touching theirs. Types are closed — once declared, they're final.
Types win when you need union types, intersection types, or you're aliasing a primitive. You can't write interface ID = string | number — that's a type job. Interfaces win when you're modelling objects that will be implemented by classes or extended by library consumers.
The rule most senior devs follow: use interface for public API shapes and object contracts; use type for everything else.
Generics — Writing Code That Doesn't Throw Away Type Safety
Generics are the point where a lot of intermediate TypeScript developers get stuck. They understand the syntax but can't explain why you'd use them over just using any.
Here's the problem generics solve: you want to write a function that works with many types, but you want TypeScript to still know what type came out the other end. any throws away that knowledge. Generics preserve it.
Think of a generic as a placeholder that gets filled in at call time. When you call getFirstItem<string>(myArray), TypeScript locks in the type as string for that entire call. It checks inputs AND outputs against that locked-in type. With any, TypeScript shrugs and checks nothing.
In real codebases, generics appear constantly: API response wrappers, repository patterns, utility hooks in React, queue data structures, and caching layers. If you've used useState<User>() in React, you've already used generics.
The constraint syntax (<T extends SomeType>) is especially important — it says 'T can be anything, but it must have at least these properties'. This lets you write flexible utilities without giving up your safety net.
<T> and <T extends object>?' With just <T>, T could be a primitive (string, number, boolean). Adding extends object restricts T to object types only. If your function tries to access a property on T and T could be a primitive, TypeScript will warn you — rightfully so.unknown vs any vs never — The Trinity That Defines TypeScript Maturity
Nothing separates a TypeScript beginner from an intermediate developer faster than how they use any. Beginners use it everywhere to 'shut TypeScript up'. Intermediate developers know it's a code smell. Senior developers understand when unknown and never are the correct tools instead.
any is an escape hatch. It turns off type checking entirely for that variable. The compiler will never complain, but you've also lost all the protection TypeScript offers. Using any on an API response is particularly dangerous — you're essentially pretending the data is safe when you haven't validated it.
unknown is the type-safe version of any. It says 'this value exists but I don't know what it is yet — and TypeScript won't let me use it until I prove what it is.' You must narrow it (with typeof, instanceof, or a type guard) before you can do anything with it. This is perfect for data coming from external sources: API responses, JSON parsing, user input.
never is the opposite end of the spectrum — it represents something that can never happen. It's the return type of a function that always throws, or the type of a variable in a branch that TypeScript has proven is unreachable. It's most powerful in exhaustive switch statements — if you add a new union member and forget to handle it, TypeScript will surface a never error that tells you exactly where.
Type Guards & Narrowing — How TypeScript Actually Gets Smarter Mid-Function
Type narrowing is the mechanism that makes TypeScript feel intelligent. You start with a wide type — string | number | null — and through checks inside your function, TypeScript progressively narrows its understanding of what the value actually is.
This isn't magic — TypeScript reads your control flow and updates the type at each branch. After an if (typeof value === 'string') check, TypeScript knows inside that block that value is definitely a string. This is called control flow analysis.
Built-in narrowing uses typeof, instanceof, in, and equality checks. But the really powerful tool is the custom type guard — a function that returns value is SomeType. When that function returns true, TypeScript accepts the narrowing and updates the type in the calling scope.
Type guards are essential when working with discriminated unions — a pattern where each member of a union has a shared kind or type field that uniquely identifies it. This pattern is everywhere in Redux actions, API event types, WebSocket messages, and state machines.
kind or type field are the TypeScript equivalent of the strategy pattern. They're the go-to for modelling state machines, Redux actions, and WebSocket event types. If you model these correctly, TypeScript prevents you from ever accessing a field that doesn't exist on the current variant — zero runtime undefined errors.The 'any' Escape Hatch — When You've Already Lost
Competitors treat 'any' like a type. It's not. It's a surrender flag. When you annotate a variable with 'any', you're telling TypeScript 'I don't care enough to figure this out, just get out of my way'. That's fine for a one-off migration script. It's a disaster in production.
The real problem is that 'any' is viral. One 'any' argument in a function poisons the return type. Suddenly your entire call chain is unchecked. I've debugged midnight outages caused by a single 'any' in an API client that swallowed a shape change.
You have two better options. Use 'unknown' when you genuinely don't know the shape at compile time — that forces you to validate before use. Use a branded type or union when you know the possible shapes. 'Any' is for the five minutes it takes you to write the proper type. After that, kill it.
Optional Properties — The Silent Undefined
Competitors ask 'Can we specify optional properties?' Yes. But the real question is: do you understand the downstream chaos they cause? An optional property is a contract that says 'this field might not exist'. That means every consumer needs a null check.
I've seen a junior add one optional field to a shared response type and break five screens. The worst part? No compile errors — because every consumer used optional chaining 'just in case'. The property was always present, but nobody knew that. The code was full of '?.'' checks that hid a design smell.
Better approach: model states as discriminated unions. If a user can have a 'billingAddress' or not, don't make it optional. Make the user type a union of 'HasBilling | NoBilling'. Now the compiler enforces the branching. Optional properties should be the exception, not the default.
Arrays — More Than Just T[]
Competitors explain arrays with 'const arr: number[] = [1,2,3]'. That's the syntax. It's not the interesting part. The interesting part is what happens when you have a heterogeneous array or a read-only constraint.
In production payment systems, I've seen 'Array<any>' propagate because someone had mixed types and couldn't be bothered to write the tuple. That's where runtime crashes come from. If your array has a fixed structure — like a coordinate pair or a key-value entry — use a tuple: '[string, number]' not 'any[]'.
Also: prefer 'ReadonlyArray<T>' when a list shouldn't be mutated. It's a compile-time promise that prevents accidental 'push' or 'sort' calls. Combine it with 'as const' for literal tuples that are truly immutable. Every time I see a mutable array in a module that doesn't own the data, I know someone's going to get a surprise mutation bug.
Index Signatures — When Your Object Keys Are Strangers
Index signatures are how you tell TypeScript 'I don't know the exact keys, but I know the shape of the values.' You see them everywhere in config maps, caches, and API response wrappers. The syntax is [key: string]: SomeType — and that key is never used at runtime, it's just a type hint for the signature.
The WHY: without index signatures, you'd be forced to use Record<string, any> or cast away safety. They let you model dynamic property access while keeping value types strict. But here's the trap — once you add an index signature, TypeScript assumes every property access returns that type. So obj.nonexistent won't error. That's why you pair them with undefined in the value type or use noUncheckedIndexedAccess in tsconfig.
In production, reach for index signatures when you're building lookup tables, normalized state stores, or any map where keys are user-generated. They're a clean escape for genuinely dynamic data — not a crutch for lazy typing.
SomeType, not SomeType | undefined. Turn on noUncheckedIndexedAccess or make undefined explicit in your value type — otherwise obj.misspelledKey silently compiles.Contravariance — Why Your Callback Types Are Backwards
Function type variance answers: if a Cat is a subtype of Animal, is (cat: Cat) => void a subtype of (animal: Animal) => void? Intuitively no — because a function that expects a specific Cat shouldn't be passed a generic Animal. That's contravariance: function parameters go in the opposite direction of their types.
Return types are covariant: if the function returns Cat, it's safe to treat it as returning Animal. But parameters? They're contravariant. TypeScript historically allowed bivariance (both directions) for method signatures in classes — a known soundness hole. That's why --strictFunctionTypes exists and why you should write arrow functions in callbacks.
In reality, you hit this when defining event handlers or reducers. A function that handles MouseEvent can't safely replace one that handles Event — because the caller might pass a KeyboardEvent. Reverse the reasoning: a function accepting Event can stand in for one accepting MouseEvent. Checks out? Good. You're thinking contravariantly.
Mixins — Composing Behavior Without Inheritance Chains
Mixins let you compose reusable behaviors from multiple classes into a single class, avoiding deep inheritance hierarchies. TypeScript doesn't have built-in mixin syntax like some languages, but you can implement them using constructor functions and class expressions. The core pattern: define a function that takes a base class and returns a new class extending it with added methods. You chain these functions to compose behavior. This bypasses single-inheritance limits and keeps code DRY. Why? Because inheritance chains become brittle as requirements grow; mixins let you mix and match capabilities like logging, serialization, or validation without coupling. The cost: TypeScript loses perfect type inference for mixins — you often need to explicitly annotate return types or use helper types. Never overuse mixins; they obscure control flow. Production reality: mixins solve cross-cutting concerns better than deep inheritance, but prefer composition via dependency injection when the behavior needs runtime substitution.
Function Overloading — TypeScript’s Static-Only Solution to JS Limitations
JavaScript doesn't support function overloading — you can't have multiple functions with the same name differing by parameters. TypeScript solves this at compile time with overload signatures. You write multiple function signatures (no bodies) above a single implementation signature with a union-typed body. The overloads tell TypeScript which argument patterns are valid; the implementation handles all cases. Why does this matter? It gives callers precise return types based on inputs, while the runtime keeps JavaScript's single-function reality. Only the implementation signature is compiled. Overloads are pure type-system decoration. Never use overloads for fundamentally different behaviors — that signals a design problem. Real-world use: parsing functions, API clients, or event handlers where argument shape changes return type. The trap: overloads don't throw runtime errors on invalid inputs — you must validate manually. Production rule: 3 overloads max; beyond that, refactor to generics or union types.
Installing TypeScript — The Minimum You Actually Need
To write and run TypeScript, you need Node.js (version 14 or later) and npm (bundled with Node). The minimum install is one command: npm install -g typescript. This gives you the tsc compiler. Verify with tsc --version. That's it — no extra tooling required. You can compile a single file: tsc hello.ts outputs hello.js. Why this matters: most tutorials assume you need tsconfig.json, ts-node, or build tools. You don't. For production, you'll want a tsconfig.json to control strictness, module resolution, and output targets. But the interview answer: Node.js + npm + typescript package. Common trap: installing TypeScript globally vs locally. Global is fine for quick tests; projects should use local installs (npm install --save-dev typescript) to lock versions. Minimum requirements: Node 14+ (Node 18+ recommended for modern features), npm, and typescript. No editors, no bundlers, no frameworks. Just compile and run.
npm install typescript is the minimum — no tsconfig or bundler required to start compiling.Key takeaways
interface for object shapes that classes implement or libraries extendtype for unions, intersections, and primitive aliases.any where a generic would work.unknown is any with a safety lockkind field turn TypeScript's control flow analysis into a compile-time state machine validatorInterview Questions on This Topic
Frequently Asked Questions
20+ years shipping production code across the stack, with years spent interviewing engineers. Lessons pulled from things that broke in production.
That's JavaScript Interview. Mark it forged?
11 min read · try the examples if you haven't