Interview prep · Junior → Senior
TypeScript Interview Questions
The questions a TypeScript interviewer actually asks about the type system and the toolchain —
types and inference, interfaces and type aliases, generics, narrowing, any/unknown/never,
the utility, mapped and conditional types, structural typing, enums and assertions, declaration files and
tooling, and decorators and the classic gotchas — each answered with a short example and a link to
the source. This page assumes JavaScript fluency and stays on what TypeScript adds; for the
language itself (closures, this, the event loop, prototypes, the DOM) see the
JavaScript interview page.
Pair it with the
TypeScript version reference
and the open TypeScript roles on the jobs board.
Difficulty
Showing 0 of 0 questions
No questions match that combination. .
1 · Types & inference
What is the difference between a type annotation and type inference? Junior
An annotation (: T) tells the compiler a value's type explicitly; inference is the compiler working it out from the initializer or surrounding context. TypeScript infers aggressively, so idiomatic code annotates only where inference can't reach — function parameters, a return type you want to pin, and variables declared without an initializer. Re-stating what TypeScript already knows (const n: number = 1) is just noise.
let count = 0; // inferred: number const name = "Ada"; // inferred: "Ada" (a literal type, because const) function add(a: number, b: number) { // params need an annotation return a + b; // return type inferred as number } let result; // inferred: any — no initializer; annotate it
Why it's asked / follow-up: it checks whether you let the compiler do its job instead of over-annotating. Follow-up: “when must you annotate?” — function parameters (there's no context to infer from) and any variable declared with no initializer (otherwise it silently becomes any).
Source: TS Handbook — Everyday Types.
What are the primitive types in TypeScript? Junior
TypeScript layers types over JavaScript's runtime primitives: string, number (every number, including floats — there's no separate int), boolean, null, undefined, symbol, and bigint. Always use the lowercase names — the capitalized String/Number refer to the boxed wrapper objects and are almost always a mistake. null and undefined only behave as distinct, must-handle types under strictNullChecks.
let s: string = "hi"; let n: number = 3.14; // one number type — no int/float split let big: bigint = 100n; let sym: symbol = Symbol(); let wrong: String = "no"; // String (capital) is the wrapper object — avoid
Why it's asked / follow-up: a fluency check that also surfaces the string-vs-String trap. Follow-up: “why one number type?” — it mirrors JavaScript's single IEEE-754 double; bigint is the separate arbitrary-precision integer type. The non-nullable behavior arrived with TypeScript 2.0's strictNullChecks.
Source: TS Handbook — Everyday Types.
What is the difference between an array type and a tuple? Junior
An array type (number[] or Array<number>) is a homogeneous, variable-length list. A tuple ([string, number]) is a fixed-length, ordered list typed by position — element 0 is a string, element 1 a number. Tuples support optional ([string, number?]) and rest ([string, ...number[]]) elements and optional element labels; as const makes a readonly tuple of literal types.
const xs: number[] = [1, 2, 3]; // any length, all numbers const pair: [string, number] = ["a", 1]; // exactly 2, typed by position const point: [x: number, y: number] = [10, 20]; // labeled tuple const rgb = [255, 0, 0] as const; // readonly [255, 0, 0]
Why it's asked / follow-up: tuples are how you type a function that returns several values (or a React useState pair). Follow-up: “what does a labeled tuple buy you?” — names in editor tooltips and signatures, with no runtime effect.
What is the difference between object, {}, and unknown?
Mid
object is any non-primitive (arrays, functions, objects — not string/number/etc.). {} is the surprisingly wide “any value that isn't null or undefined” — it accepts primitives too, because every non-nullish value trivially satisfies “has no required members.” unknown is the genuine any-value top type. None of the three lets you touch a property without narrowing first.
let o: object = { a: 1 }; o = [1]; // ok; o = 1 → error (primitive) let e: {} = 1; // ok! {} accepts anything but null/undefined let u: unknown = 1; // ok; the safe "any value" type // e.toFixed(2); // error — {} has no known members
Why it's asked / follow-up: people reach for {} expecting “an object” and get something close to any. Follow-up: “what should you use for ‘some object’?” — Record<string, unknown> or an explicit shape, not {}.
What do void and never mean?
Mid
void is the return type of a function that produces no useful value (it may return; or just fall off the end). never is the type of a value that never occurs — a function that never returns (it throws or loops forever), or a value narrowed to impossible. A key asymmetry: a void-typed callback accepts a function that returns something (the value is discarded — which is why arr.forEach(x => arr.push(x)) type-checks), while never is assignable to every type but nothing is assignable to it.
function log(msg: string): void { console.log(msg); } // returns nothing function fail(m: string): never { throw new Error(m); } // never returns const cb: () => void = () => 42; // ok — return value is discarded let n: never; n = 1; // error: nothing is assignable to never
Why it's asked / follow-up: never underpins exhaustiveness checking, and the void “return is ignored” rule explains a common confusion. Follow-up: “where does never show up by accident?” — an empty array literal can infer never[] with no annotation. Both never and control-flow analysis arrived with TypeScript 2.0.
2 · Interfaces vs type aliases
What is the difference between an interface and a type alias?
Junior
Both name a shape. The practical differences: an interface can be re-opened and merged (declare it twice and the members combine — declaration merging) and is restricted to object/function shapes; a type alias is a single closed definition but can name anything — unions, intersections, primitives, tuples, mapped and conditional types. Rule of thumb: reach for interface for public object/class contracts (extendable, clearer errors), type when you need a union or a computed type.
interface User { id: number; } interface User { name: string; } // merges → { id; name } type ID = string | number; // a union — interfaces can't do this type Point = { x: number; y: number };
Why it's asked / follow-up: it's the most-asked TypeScript design question. Follow-up: “which for a library's public API?” — interface, because consumers can augment it and the error messages are clearer; type when the shape is a union or otherwise not a plain object.
What is declaration merging? Mid
TypeScript merges multiple declarations of the same name in the same scope into one. Two interfaces with the same name combine their members; a namespace can merge with a class, function, or enum to add static members. type aliases do not merge — a duplicate is an error. This is the mechanism behind module augmentation (adding a property to Window, extending a library's types).
interface Box { height: number; } interface Box { width: number; } const b: Box = { height: 1, width: 2 }; // both required type T = { a: number }; // type T = { b: number }; // error: Duplicate identifier 'T'
Why it's asked / follow-up: it's the “why can I declare an interface twice” question and the foundation of augmentation. Follow-up: “can you merge a type?” — no, which is a concrete reason to choose interface for an extensible contract.
Source: TS Handbook — Declaration Merging.
interface extends vs. an intersection type (&) — what's the difference?
Mid
Both combine shapes. interface B extends A is a declared inheritance relationship, checked eagerly — a conflicting property is an error at the declaration. An intersection A & B computes a new type and, on a conflicting property, resolves that member to never rather than erroring up front. extends only works on interfaces/classes; & works on any types (including unions and primitives). On large hierarchies the compiler caches interface extends better.
interface A { x: number } interface B extends A { y: number } // { x; y } type C = { x: number } & { y: number }; // { x; y } type Bad = { p: string } & { p: number };// p: never (string & number)
Why it's asked / follow-up: it checks whether you know the conflict behavior differs. Follow-up: “which scales better?” — interface extends for deep hierarchies (cached), & for ad-hoc composition and when a member is a union or primitive.
Source: TS Handbook — Object Types (Extending & Intersection).
3 · Generics
What are generics, and why use them? Junior
Generics let a function, class, or type take a type parameter so it works over many types while preserving the relationship between inputs and outputs. Without them you'd either duplicate code per type or fall back to any (which discards all type info). The point is that the return type tracks the argument type instead of collapsing to any.
function first<T>(arr: T[]): T | undefined { return arr[0]; } const n = first([1, 2, 3]); // T = number → number | undefined const s = first(["a"]); // T = string → string | undefined interface Box<T> { value: T; } // generic interface const b: Box<string> = { value: "x" };
Why it's asked / follow-up: generics are everywhere in real TypeScript (collections, promises, React props). Follow-up: “generics vs any?” — any throws type info away; a generic carries it through, so the result stays precisely typed.
Source: TS Handbook — Generics.
How do generic constraints work (T extends …)?
Mid
A constraint with extends restricts what a type parameter can be, which lets you safely use members of it inside the generic. T extends { length: number } means “any T that has a length,” so reading .length is allowed. Constraints also power keyof-based generics (K extends keyof T) for type-safe property access.
function longest<T extends { length: number }>(a: T, b: T): T { return a.length >= b.length ? a : b; } longest([1, 2], [1, 2, 3]); // ok (arrays have length) → number[] // longest(1, 2); // error: number has no 'length' function prop<T, K extends keyof T>(o: T, k: K): T[K] { return o[k]; }
Why it's asked / follow-up: it's the step from “what is a generic” to “use one safely.” Follow-up: “what does K extends keyof T give you?” — prop(user, "id") returns exactly T["id"] (here number), not any. keyof arrived in TypeScript 2.1.
Source: TS Handbook — Generic Constraints.
What are default type parameters? Mid
A type parameter can carry a default (<T = string>) used when the caller supplies none and none can be inferred. It's the type-level analog of a default function argument — common in generic containers and React component generics so the common case needs no explicit type argument.
interface Container<T = string> { items: T[]; } const a: Container = { items: ["x"] }; // T defaults to string const b: Container<number> = { items: [1] }; // explicit override function make<T = number>(): T[] { return []; } const xs = make(); // number[]
Why it's asked / follow-up: it shows up in real generic API design. Follow-up: “default vs constraint?” — a default supplies a fallback type; a constraint limits the allowed types; you can combine them (<T extends object = {}>).
Source: TS Handbook — Generics (Generic Parameter Defaults).
When is a generic over-engineering? Senior
A type parameter earns its place only when it ties two things together — an input to an output, or one parameter to another. If a type variable appears in a signature exactly once, it almost always conveys nothing and could be unknown (or the constraint) instead. Reaching for generics where a union or a plain parameter would do makes signatures harder to read with no safety gain.
// Pointless: T appears once, conveys nothing over `unknown`. function logIt<T>(x: T): void { console.log(x); } function logIt2(x: unknown): void { console.log(x); } // same contract // Justified: T links the argument type to the return type. function wrap<T>(x: T): T[] { return [x]; }
Why it's asked / follow-up: it separates “can write generics” from “knows when not to.” Follow-up: “what's the rule of thumb?” — a type parameter used in only one position is a code smell; if it doesn't relate two things, drop it.
Source: TS Handbook — Generics.
4 · Union, intersection & narrowing
What are union and intersection types? Junior
A union A | B is “either A or B” — a value that could be one of several types; you must narrow before using type-specific members. An intersection A & B is “both at once” — a value carrying all members of both, used to compose object shapes. Unions are the more common, and the reason narrowing exists.
type Id = string | number; // union: one OR the other function pad(x: Id) { // x.toUpperCase(); // error — number has no toUpperCase if (typeof x === "string") x.toUpperCase(); // narrowed to string } type Draggable = { drag(): void }; type Widget = Draggable & { resize(): void }; // intersection: has both
Why it's asked / follow-up: unions plus narrowing are the spine of everyday TypeScript. Follow-up: “why can't you call a string method on string | number?” — only members common to every member of the union are available until you narrow.
Source: TS Handbook — Union Types.
What are type guards, and how do you write a custom one? Mid
A type guard is an expression that narrows a type within a block. The built-ins are typeof (primitives), instanceof (classes), in (property presence), and truthiness/equality checks. For richer logic you write a user-defined type guard — a function returning a type predicate x is T; when it returns true, the compiler narrows the argument to T.
interface Cat { meow(): void } interface Dog { bark(): void } function isCat(a: Cat | Dog): a is Cat { // type predicate return "meow" in a; } function speak(a: Cat | Dog) { if (isCat(a)) a.meow(); // narrowed to Cat else a.bark(); // narrowed to Dog }
Why it's asked / follow-up: custom guards narrow beyond what typeof/instanceof can express. Follow-up: “what if the predicate lies?” — TypeScript trusts it, so a wrong x is T is an unsound escape hatch (like as). TypeScript 5.5 can also infer some predicates automatically.
What is a discriminated (tagged) union? Mid
A union of object types that share a common literal-typed property (the discriminant, or tag). Switching on the tag narrows the whole object to the matching member, giving exhaustive, type-safe handling. It's the idiomatic TypeScript way to model “one of N shapes” — far safer than optional fields and casts.
type Shape = | { kind: "circle"; r: number } | { kind: "square"; size: number }; function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.r ** 2; // narrowed to circle case "square": return s.size ** 2; // narrowed to square } }
Why it's asked / follow-up: discriminated unions are the headline pattern for modeling state. Follow-up: “how do you guarantee every case is handled?” — an exhaustiveness check with never (next question).
Source: TS Handbook — Discriminated Unions.
How do you do exhaustiveness checking? Senior
In the default/else branch of a discriminated-union switch, assign the value to a never. A fully handled union narrows to never there, so this compiles — but the day someone adds a new member, that branch is no longer never and the assignment errors, forcing them to handle the new case. It turns “forgot a case” into a compile error.
type Shape = { kind: "circle"; r: number } | { kind: "square"; size: number }; function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.r ** 2; case "square": return s.size ** 2; default: const _exhaustive: never = s; // errors if a new kind is added return _exhaustive; } }
Why it's asked / follow-up: it's the senior payoff of never plus discriminated unions. Follow-up: “why never and not a runtime throw?” — a throw catches the miss at runtime; the never assignment catches it at compile time, before you ship. The never type is from TypeScript 2.0.
5 · any, unknown & never
Explain any, unknown, and never.
Mid
any opts a value out of type-checking entirely — anything is assignable to it and you can do anything with it (an escape hatch that disables safety and spreads). unknown is the type-safe top type — anything is assignable to it, but you can't do anything with it until you narrow. never is the bottom type — the empty set; assignable to everything, but nothing (except never) is assignable to it. Mental model: unknown is “I don't know yet, make me check”; any is “stop checking”; never is “this can't happen.”
let a: any = 4; a.foo.bar(); // compiles — checking disabled let u: unknown = 4; // u.toFixed(); // error — must narrow first if (typeof u === "number") u.toFixed(); // ok after narrowing let n: never; // can hold no value at all
Why it's asked / follow-up: this is the signature TypeScript interview cluster. Follow-up: “default to which?” — unknown for untyped/external input; any only as a deliberate, localized escape hatch. unknown arrived in TypeScript 3.0; never in 2.0.
Source: TS Handbook — Everyday Types (any) / TS 3.0 release notes (unknown).
Why prefer unknown over any for untyped input?
Mid
Both accept any value, but any silently disables checking on everything it touches — it propagates, so one any can quietly poison a whole call chain and re-introduce the runtime errors TypeScript exists to prevent. unknown keeps the value opaque: you must narrow (a typeof check, a type guard, a schema parse) before using it, so the compiler still protects every downstream use. That's why JSON.parse-style boundaries and catch clauses are best typed unknown.
function parse(json: string): unknown { return JSON.parse(json); } const data = parse('{"n":1}'); // data.n; // error — narrow first try { /* ... */ } catch (e) { // e is unknown under strict if (e instanceof Error) console.log(e.message); }
Why it's asked / follow-up: it's the practical “why does unknown exist” question. Follow-up: “what flips catch variables to unknown?” — useUnknownInCatchVariables, on under strict since TypeScript 4.4 (before that, catch bindings were any). The unknown type is from TypeScript 3.0.
What is never actually used for?
Senior
Three practical uses. (1) The return type of functions that never finish (throw or loop forever). (2) The exhaustiveness-check assignment that makes a missing union case a compile error. (3) As a building block in conditional/mapped types to remove members — resolving a distributed conditional to never filters that union member out, which is exactly how Exclude and NonNullable are built.
type NoNull<T> = T extends null | undefined ? never : T; type A = NoNull<string | null>; // string type MyExclude<T, U> = T extends U ? never : T; type B = MyExclude<"a" | "b" | "c", "a">; // "b" | "c"
Why it's asked / follow-up: it shows you understand never as the bottom type, not just “the throw type.” Follow-up: “why does never disappear from a union?” — T | never simplifies to T, so resolving a conditional branch to never drops that member. See conditional types (TS 2.8).
Source: TS Handbook — Conditional Types.
6 · Utility, mapped & conditional types
What are utility types? Name a few you use. Mid
Built-in generic type transformers (in lib.d.ts) that derive new types from existing ones, so you don't re-declare shapes. The common ones: Partial<T> (all props optional), Required<T>, Readonly<T>, Pick<T, K> / Omit<T, K> (select / drop keys), Record<K, V> (object with typed keys and values), ReturnType<F> / Parameters<F> (extract from a function type), Awaited<T> (unwrap a Promise), and Exclude / Extract / NonNullable (filter unions).
interface User { id: number; name: string; email: string; } type Draft = Partial<User>; // all optional type Public = Omit<User, "email">; // { id; name } type Lookup = Record<string, User>; // { [k: string]: User } type R = ReturnType<() => User>; // User
Why it's asked / follow-up: utility types are everyday TypeScript and the gateway to mapped/conditional types (they're built from them). Follow-up: “Pick vs Omit?” — Pick keeps the named keys, Omit drops them; under the hood Omit<T,K> is Pick<T, Exclude<keyof T, K>>. Mapped types arrived in TypeScript 2.1.
Source: TS Handbook — Utility Types.
What do keyof, indexed access (T[K]), and typeof do at the type level?
Mid
keyof T produces the union of T's property names as a type. Indexed access T[K] looks up the type of property/properties K on T. The type-level typeof x captures the type of a value (distinct from JavaScript's runtime typeof). Together they let types track real objects — derive a type from a config object, or write a type-safe accessor.
interface User { id: number; name: string; } type Keys = keyof User; // "id" | "name" type IdType = User["id"]; // number (indexed access) const config = { port: 8080, host: "localhost" }; type Config = typeof config; // { port: number; host: string } type Port = Config["port"]; // number
Why it's asked / follow-up: these three operators are the foundation of mapped and conditional types. Follow-up: “what's T[keyof T]?” — the union of all value types in T (here number | string). keyof is from TypeScript 2.1.
Source: TS Handbook — keyof / Indexed Access Types.
What is a mapped type? Senior
A type that builds a new object type by iterating the keys of another with [K in Keys] — it's how Partial, Readonly, etc. are defined. You can add or remove the ? (optional) and readonly modifiers with +/-, and (since TypeScript 4.1) remap keys with an as clause plus template literal types.
type MyPartial<T> = { [K in keyof T]?: T[K] }; // add ? type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // strip readonly type Getters<T> = { // key remapping (4.1+) [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }; type G = Getters<{ name: string }>; // { getName: () => string }
Why it's asked / follow-up: mapped types are the senior “do you understand how the utility types work” probe. Follow-up: “how do you remove readonly/??” — the -readonly and -? modifiers. Mapped types are from TS 2.1; key remapping from TS 4.1.
Source: TS Handbook — Mapped Types.
What are conditional types and infer?
Senior
A conditional type chooses between two types based on an extends test: T extends U ? X : Y. infer introduces a fresh type variable inside the extends clause to capture part of a matched type — that's how ReturnType pulls the return type out of a function. Conditional types also distribute over a naked union type parameter: applied to A | B, the test runs separately on A and B.
type ElementType<T> = T extends (infer U)[] ? U : T; // infer the element type A = ElementType<string[]>; // string type B = ElementType<number>; // number (not an array → T) type MyReturn<F> = F extends (...args: any[]) => infer R ? R : never; type C = MyReturn<() => User>; // User
Why it's asked / follow-up: conditional types plus infer are the heart of advanced library types (Zod, tRPC). Follow-up: “what's distribution?” — a conditional over a naked union type parameter runs member-by-member; wrap the check in [T] to opt out. Conditional types and infer are from TypeScript 2.8.
Source: TS Handbook — Conditional Types.
What are template literal types? Senior
Types built with template-string syntax that produce string-literal types from other string types (TypeScript 4.1+). Combined with unions they expand combinatorially, and the intrinsic helpers Uppercase/Lowercase/Capitalize/Uncapitalize transform string literals. They power typed event names, CSS-property keys, and the key remapping in mapped types.
type Lang = "en" | "fr"; type Greeting = `hello_${Lang}`; // "hello_en" | "hello_fr" type EventName<T extends string> = `${T}Changed`; type E = EventName<"name">; // "nameChanged" type Loud = Uppercase<"hi">; // "HI"
Why it's asked / follow-up: template literal types are a modern-TypeScript signal and the partner to mapped-type key remapping. Follow-up: “where do they show up in practice?” — typed event emitters, route params, and as-remapped getters/setters. They arrived in TypeScript 4.1.
Source: TS Handbook — Template Literal Types.
7 · Structural typing
What is structural typing, and how is it different from nominal typing? Mid
TypeScript types are structural: compatibility is decided by shape, not by name or declared inheritance. If an object has at least the required members of a type, it is that type — no implements needed (“duck typing”). Nominal languages (Java, C#) decide by the declared name, so two identically-shaped classes are unrelated. Structural typing is why a plain object can satisfy an interface it never mentions.
interface Named { name: string; } class Person { constructor(public name: string) {} } const p: Named = new Person("Ada"); // ok — Person has a 'name' const raw = { name: "Lin", age: 30 }; const n: Named = raw; // ok — extra 'age' is fine via a variable function greet(x: Named) {} greet(raw); // ok — no 'implements' needed
Why it's asked / follow-up: it's a foundational mental-model question many miss. Follow-up: “the consequence?” — unrelated types with the same shape are interchangeable, usually convenient but occasionally surprising; branded types fix the cases where you want nominal behavior.
Source: TS Handbook — Type Compatibility.
What are excess property checks? Mid
Normally structural typing allows extra properties (a value with more members than required is still assignable). But when you assign a fresh object literal directly to a typed target, TypeScript applies a stricter excess-property check and flags properties the target doesn't declare — to catch typos like colour vs color. Assigning through an intermediate variable, or using an index signature, bypasses it.
interface Opts { width: number; } // const a: Opts = { width: 1, height: 2 }; // error: 'height' not in Opts const tmp = { width: 1, height: 2 }; const b: Opts = tmp; // ok — not a fresh literal function f(o: Opts) {} f({ width: 1, color: "red" } as Opts); // bypassed via assertion
Why it's asked / follow-up: it's the “why does my object literal error but the variable doesn't” puzzle. Follow-up: “how do you allow extras intentionally?” — add an index signature ([k: string]: unknown) to the target type.
Source: TS Handbook — Excess Property Checks.
How do you get nominal typing in TypeScript (branded types)? Senior
Because TypeScript is structural, two aliases like type UserId = string and type OrderId = string are interchangeable — a bug if you never want to mix them. The fix is branding: intersect the base type with a phantom property (a unique symbol or a string-literal tag) that exists only at the type level. The brand makes the types incompatible without a deliberate cast, while the runtime value stays a plain string/number.
type Brand<T, B> = T & { readonly __brand: B }; type UserId = Brand<string, "UserId">; type OrderId = Brand<string, "OrderId">; const toUserId = (s: string) => s as UserId; let u: UserId = toUserId("u1"); // let o: OrderId = u; // error — branded types don't mix
Why it's asked / follow-up: it's the senior “you understand structural typing well enough to defeat it on purpose” question. Follow-up: “runtime cost?” — none; the brand is erased, so it's a purely compile-time guard.
Source: TS Handbook — Type Compatibility (branding is a community idiom built on structural typing).
8 · Enums, literals & assertions
How do TypeScript enums work, and what are the alternatives? Mid
enum creates a named set of constants. Numeric enums auto-increment from 0 and emit a two-way (reverse) runtime mapping; string enums map to string values and are one-way. Enums are one of the few TypeScript features that emit real runtime code (an object), unlike most of the type layer. Many teams instead prefer a union of string literals (or an as const object) — lighter, fully erasable, and free of numeric-enum footguns.
enum Dir { Up, Down } // Up=0, Down=1; Dir[0] === "Up" at runtime enum Color { Red = "RED" } // string enum — one-way // Popular alternative — a literal union (no runtime emit): type Dir2 = "up" | "down"; const Dir3 = { Up: "up", Down: "down" } as const; type Dir4 = (typeof Dir3)[keyof typeof Dir3]; // "up" | "down"
Why it's asked / follow-up: the “enum vs union of literals” debate is a common opinion probe. Follow-up: “why avoid const enum?” — it inlines values at compile time, which breaks under single-file transpilers (Babel/esbuild) and is disallowed by isolatedModules.
Source: TS Handbook — Enums.
What does as const do?
Mid
A const assertion. It tells TypeScript to infer the narrowest, immutable type for a literal: string/number literals stay literal (not widened to string/number), object properties become readonly, and arrays become readonly tuples. It's how you turn a config object or an array of literals into a precise type you can derive unions from.
let a = "GET"; // widened to string let b = "GET" as const; // type "GET" const route = { method: "GET", path: "/x" } as const; // route.method = "POST"; // error — readonly const dirs = ["up", "down"] as const; // readonly ["up","down"] type Dir = (typeof dirs)[number]; // "up" | "down"
Why it's asked / follow-up: as const is everywhere in modern typed config. Follow-up: “what's literal widening?” — without as const, a let or object property widens a literal to its base type; the assertion freezes it. Const assertions arrived in TypeScript 3.4.
What's the difference between as and the satisfies operator?
Senior
as T is a type assertion — it tells the compiler “trust me, treat this as T,” overriding inference (and it can be wrong, hiding bugs). satisfies T (TypeScript 4.9) checks that an expression conforms to T without changing its inferred type — so you get validation plus the precise literal type. Use satisfies for “make sure this matches the contract but keep the narrow type”; use as only as a deliberate escape hatch.
type Config = Record<string, number | string>; const c1 = { port: 8080 } as Config; // type widened to Config // c1.port.toFixed(); // error — port is number|string now const c2 = { port: 8080 } satisfies Config; // validated AND keeps {port: number} c2.port.toFixed(); // ok — port is number
Why it's asked / follow-up: satisfies is a modern-TypeScript signal and the cleaner replacement for many as uses. Follow-up: “when is as still right?” — narrowing unknown after a runtime check, DOM casts (as HTMLInputElement), and other places you genuinely know more than the compiler. The satisfies operator is from TypeScript 4.9.
What does the non-null assertion operator (!) do?
Mid
x! tells the compiler “I know x is not null/undefined here,” removing those from its type. It's a compile-time assertion only, with no runtime check (it's erased). Useful when you know more than the flow analysis (a value initialized elsewhere), but unsound: if you're wrong you get a runtime error the types said couldn't happen. Prefer a real check or optional chaining where practical.
function len(s?: string) { // return s.length; // error — s may be undefined return s!.length; // asserts non-null — no runtime guard, can still crash } const el = document.getElementById("app")!; // common DOM use
Why it's asked / follow-up: it probes whether you know ! is unchecked. Follow-up: “! vs ?.?” — ?. checks at runtime and short-circuits to undefined; ! only silences the compiler and does nothing at runtime. The operator arrived with TypeScript 2.0.
9 · Declaration files, modules & tooling
What is a .d.ts declaration file, and what is @types?
Mid
A .d.ts file contains type declarations only — no implementations. It describes the shape of JavaScript so TypeScript can type-check against it: the types for a plain-JS library, ambient globals, or the .d.ts your own build emits (--declaration) for consumers. declare introduces a type-only declaration with no body. For libraries that don't ship their own types, the community-maintained DefinitelyTyped repo publishes them under the @types/* npm scope (@types/node, @types/lodash).
// globals.d.ts — ambient declaration, no implementation declare const APP_VERSION: string; declare function gtag(...args: unknown[]): void; declare module "untyped-lib" { // shim a library with no types export function doThing(x: number): string; } // npm i -D @types/lodash ← types for a JS library, from DefinitelyTyped
Why it's asked / follow-up: declaration files are how TypeScript meets the untyped-JS world. Follow-up: “a library has no types?” — install @types/<pkg> if it exists on DefinitelyTyped, otherwise write a local declare module shim.
Source: TS Handbook — Declaration Files.
What is module augmentation? Senior
Using declaration merging across module boundaries to add members to an existing module's or interface's types — without forking it. You re-declare module "x" (or declare global) and add to an interface, and TypeScript merges your additions with the original. It's how you add a property to Window, extend Express's Request, or add a method to a library's interface.
// augment the global Window declare global { interface Window { myAnalytics: (e: string) => void; } } window.myAnalytics("load"); // now typed import "express"; declare module "express" { interface Request { userId?: string; } }
Why it's asked / follow-up: it's the senior “you understand declaration merging deeply” question. Follow-up: “interface vs type for this?” — only interface (and namespace) merge; you can't augment a type alias, a concrete reason libraries expose interfaces.
Source: TS Handbook — Module Augmentation.
What does strict mode (and strictNullChecks) turn on?
Mid
"strict": true in tsconfig.json turns on the whole family of strict flags at once — most importantly strictNullChecks (null/undefined are no longer silently part of every type; you must handle them), plus noImplicitAny, strictFunctionTypes, strictPropertyInitialization, useUnknownInCatchVariables, and more. It's the single most consequential setting: without strictNullChecks, the type system can't catch the “cannot read property of undefined” class of bugs that motivate using TypeScript at all.
// tsconfig.json → { "compilerOptions": { "strict": true } } // with strictNullChecks ON: function f(s: string | null) { // s.length; // error — s could be null return s?.length ?? 0; // handle the null }
Why it's asked / follow-up: it's the “do you run TypeScript in the mode that actually helps” question. Follow-up: “why is non-strict dangerous?” — null/undefined become assignable to everything, so the compiler greenlights the exact crashes TypeScript is meant to prevent. strictNullChecks arrived in TypeScript 2.0.
Source: TSConfig Reference — strict.
Do TypeScript types exist at runtime? Mid
No. TypeScript erases all type annotations, interfaces, type aliases, and generics during compilation — the emitted JavaScript has no type information. So you cannot test a type at runtime, branch on a generic T, or instanceof an interface. Runtime checks must use JavaScript values: typeof, instanceof (on classes, which do exist at runtime), property presence, or a schema validator. A few constructs do emit code — enum, class, and namespaces — but the type layer itself does not.
interface User { name: string } function f<T>(x: T) { // if (x instanceof User) ... // error — interface isn't a runtime value // if (typeof x === T) ... // error — T doesn't exist at runtime } // What survives: classes & enums emit JS; purely type-level code vanishes.
Why it's asked / follow-up: misunderstanding erasure causes a whole class of “why can't I check the type” bugs. Follow-up: “how do you validate external data, then?” — a runtime schema library (Zod, io-ts) that also infers a TypeScript type, so the check and the type stay in sync.
Source: TS Handbook — Erased Types.
What's the difference between type-checking with tsc and transpile-only builds (Babel, swc, esbuild)?
Senior
tsc does two jobs — type-check and emit JavaScript. Modern build tools (Babel's preset-typescript, swc, esbuild, and Node's built-in type-stripping) only strip types and emit — far faster, but they do no type checking and transform each file in isolation. The common setup: a fast transpiler builds/bundles while tsc --noEmit runs separately (in CI and the editor) as the type-checker. That single-file model is why those tools can't handle constructs needing whole-program info — hence isolatedModules.
// package.json scripts { "build": "esbuild src/index.ts --bundle --outfile=out.js", // fast, no checking "typecheck": "tsc --noEmit" // checking only }
Why it's asked / follow-up: it's the “do you understand the modern TypeScript build pipeline” question. Follow-up: “what is isolatedModules?” — a flag that bans constructs a single-file transpiler can't handle (const enum inlining, certain re-exports), keeping tsc and Babel/swc/esbuild in agreement.
Source: TSConfig Reference — isolatedModules.
10 · Decorators & classic gotchas
What's the state of decorators in TypeScript? Senior
There are two systems. The old experimental decorators (behind experimentalDecorators) implement an early TC39 proposal and are what Angular, NestJS, and TypeORM still use — they also rely on emitDecoratorMetadata + reflect-metadata. TypeScript 5.0 (2023) shipped the standard Stage 3 decorators — spec-compliant, no flag needed, with a different signature. The standard ones don't yet cover parameter decorators or the metadata the legacy system emits, which is why decorator-heavy frameworks haven't fully migrated.
// Stage 3 (TS 5.0+) — no flag: function logged<T, A extends any[], R>( fn: (this: T, ...a: A) => R, ctx: ClassMethodDecoratorContext ) { return function (this: T, ...a: A): R { console.log("call", String(ctx.name)); return fn.call(this, ...a); }; } class C { @logged greet() { return "hi"; } }
Why it's asked / follow-up: the experimental-vs-standard split is a live, real-world gotcha. Follow-up: “why do Angular/Nest still use the legacy ones?” — they depend on parameter decorators and emitDecoratorMetadata, which the Stage 3 standard doesn't provide. Stage 3 decorators shipped in TypeScript 5.0.
Source: TS 5.0 release notes — Decorators / TC39 decorators proposal.
Why does Object.keys(obj) return string[] instead of (keyof T)[]?
Senior
Deliberate soundness. Because TypeScript is structural, a value typed T may at runtime hold extra properties beyond T's declared keys (a subtype was passed in). If Object.keys returned (keyof T)[], you could iterate keys that aren't actually in keyof T — unsound. So the lib types it string[] (the same reasoning hits for...in). If you know there are no excess keys, cast or write a typed helper.
const user = { id: 1, name: "Ada" }; Object.keys(user).forEach(k => { // user[k] // error — k is string, not keyof typeof user }); // typed helper when you accept the unsoundness: const keys = Object.keys(user) as (keyof typeof user)[];
Why it's asked / follow-up: it's a famous gotcha that actually teaches structural typing. Follow-up: “is the cast safe?” — only if no caller passes an object with extra properties; otherwise you can hit a key that isn't really keyof T.
Source: TypeScript FAQ — Why doesn't Object.keys return keyof?.
How does any silently leak through a codebase?
Mid
any is contagious. Any expression involving an any is itself any, and assigning an any to a typed variable disables checking there too — so one untyped boundary (a JSON.parse, an untyped library, an as any) can quietly turn off type-checking across everything downstream, re-introducing runtime bugs with zero compiler complaints. Guard the boundaries: type external input as unknown and narrow, and enable noImplicitAny so accidental anys become errors.
const data: any = JSON.parse(s); const id = data.user.id; // id is `any` — no checking id.toExponential(); // compiles; crashes if id isn't a number // fix: type the boundary as `unknown` and narrow before use
Why it's asked / follow-up: it's the practical “why is any dangerous beyond the one value” question. Follow-up: “how do you find leaks?” — noImplicitAny plus lint rules (no-explicit-any, the no-unsafe-* family); type-coverage tools report your any ratio. Prefer unknown at boundaries.
Source: TS Handbook — Everyday Types (any).
How can as hide real bugs?
Mid
A type assertion overrides the compiler's inference instead of checking against it, so an incorrect as silences errors the compiler would have caught — and the runtime then does what the values actually are, not what you asserted. TypeScript only blocks assertions between types with no overlap, and even that is bypassed with a as unknown as T double cast. Treat as as “I'm overriding type safety here,” not “I'm converting” — it changes no runtime value.
const el = document.getElementById("n") as HTMLInputElement; // could be null! el.value; // compiles; crashes if #n is missing or not an input const n = "5" as unknown as number; // double-cast escape hatch — no conversion n.toFixed(); // compiles; "5".toFixed is not a function at runtime
Why it's asked / follow-up: it's the “do you know as isn't a cast” question. Follow-up: “safer alternatives?” — satisfies for conformance, a real runtime check plus narrowing, or a user-defined type guard.
Source: TS Handbook — Type Assertions.
What runtime traps come from type erasure? Mid
Because types vanish at compile time, several things people expect to work don't: you can't instanceof an interface or type alias; you can't construct a generic's type at runtime (new T() is illegal); overload signatures don't exist at runtime (one implementation handles all); and import type is fully erased, so it can't be used as a value. The fix in each case is to keep type-driven decisions at compile time and use real JavaScript values (a discriminant property, a class, a factory) for runtime decisions.
import type { User } from "./user"; // erased — type-only // const u = new User(); // error if imported as type-only function create<T>(): T { return new T(); } // error — T has no runtime value // pass a constructor instead: function create2<T>(ctor: new () => T): T { return new ctor(); }
Why it's asked / follow-up: it makes the consequences of erasure concrete. Follow-up: “how do you construct a generic?” — take the constructor as a parameter (new () => T); the type alone can't build anything.
Source: TS Handbook — Modules (importing types) / Erased Types.
Every answer links its primary source inline — the official TypeScript documentation and handbook and, where a TypeScript feature tracks a JavaScript proposal, the TC39 spec. The questions are a curated set of the topics a TypeScript interviewer commonly covers, not a copy of any question bank. This page assumes JavaScript fluency — for the language itself see the JavaScript interview page; feature details cross-link to the TypeScript version reference. Last updated June 2026.
Mungomash LLC · More on TypeScript