TypeScript interview bank

Questions interviewers actually ask, each answered at the length you would say it aloud, with the topic to reread if you were not sure.

60 questions Junior Senior
All levels Junior Mid Senior
Reveal one by one Show all answers
Report an error

Basics

7 questions
01 When should you write a TypeScript annotation instead of relying on inference? Junior common reveal ▾ hide ▴

I annotate boundaries where the code must communicate or preserve a contract: public function parameters and returns, exported objects, empty collections, and variables whose initial value is narrower than their later states. Inside an implementation, I usually let initializers and contextual typing infer obvious locals. Repeating string or number on every constant adds noise without more safety. The key test is whether changing the implementation should be allowed to change the exposed type. If not, an annotation makes that intent explicit and lets the compiler catch accidental API drift.

read more Basic types
Was this clear?
02 How do unknown and any differ at an external data boundary? Junior common reveal ▾ hide ▴

Both types can receive any incoming value, but they impose opposite obligations. any opts out of checking, so property access, calls, and assignments continue without proof and the unsafe type spreads to callers. unknown preserves the uncertainty: code must narrow or validate it before using type-specific operations. For JSON, messages, environment-derived values, or third-party callbacks, I accept unknown, check the container and every required field, then construct the domain value. I reserve any for a documented migration seam and keep that seam as small as possible.

read more Basic types
Was this clear?
03 What JavaScript does a TypeScript enum create, and how do numeric and string enums differ? Mid common reveal ▾ hide ▴

A regular enum creates an object at runtime, unlike an interface or type alias. A string enum emits properties from member names to string values. A numeric enum emits those forward properties plus reverse properties from numbers back to names, so Object.keys and Object.values contain both directions. Duplicate numeric values share one reverse key, and the later member name wins. I inspect the emitted artifact when build configuration matters, and I filter numeric-enum iteration by runtime value type rather than asserting that every object entry is one declared member.

read more Enums
Was this clear?
04 When would you choose an enum, an as const object, or a literal union? Mid common reveal ▾ hide ▴

I start with the runtime contract. A literal union is smallest when callers only need a closed type and should pass native string literals. An as const object adds an iterable JavaScript value while deriving the union from one source of truth, which fits wire values and option lists. I choose a regular enum when the API deliberately uses enum-member identity or needs numeric reverse lookup. I avoid deciding from generic bundle-size claims because emit and tree-shaking depend on the actual compiler and bundler. Whichever representation I choose, external input still needs runtime validation.

read more Enums
Was this clear?
05 How do ES modules and TypeScript namespaces differ, and which should organize application code? Mid common reveal ▾ hide ▴

An ES module has explicit imports and exports and participates in the runtime module loader. In TypeScript, a file with a top-level import or export has its own module scope; export {} is enough to make that boundary. A namespace is a TypeScript construct that groups names, commonly emitting or describing one runtime object and supporting declaration merging. For modern application and package boundaries I use ES modules because loaders, bundlers, and package exports understand them. Namespaces remain useful for some global-script declarations and declaration-merging patterns. I use import type only for type positions because it is erased and cannot supply a runtime value.

read more
Was this clear?
47 How do structural typing and excess-property checks differ in TypeScript? Junior occasional reveal ▾ hide ▴

In TypeScript 6, object compatibility is mainly structural: a value is assignable when it has the required members, even if it has additional ones. A fresh object literal in a typed position receives an extra excess-property check, which often catches misspelled keys. Storing that object in a variable first may remove that diagnostic because ordinary structural assignability applies. This never strips properties or validates runtime input. The pitfall is treating the literal check as an exact-object guarantee; external data still needs a parser, and sensitive output needs explicit allowlisted construction.

read more Basic types
Was this clear?
50 Why is publishing an ambient const enum risky across package versions? Senior occasional reveal ▾ hide ▴

TypeScript 6 normally erases a const enum and inlines each member value at the use site. A consumer can therefore compile against version A, embed its numeric code, and execute with version B whose code changed; the runtime branch then disagrees with the source-level member. Ambient members also conflict with single-file compilation under isolatedModules. preserveConstEnums can retain an internal runtime object, but public declarations should usually be de-constified or replaced with a regular enum or constant object. The trade-off favors a stable package boundary over minor inlining benefits.

read more Enums
Was this clear?

Type system

15 questions
06 When should you use a literal union instead of string? Junior common reveal ▾ hide ▴

I use a literal union when the program owns a finite set of meaningful choices, such as request methods, workflow states, or command names. The union rejects misspellings, drives completion, and can support exhaustive control flow. I keep string when values are user-defined, server-extensible, or otherwise open. Adding string to a literal union does not provide both behaviors; string already contains every string literal, so the constraint collapses. At external boundaries, even a closed union needs runtime membership checks because TypeScript erases the type and JavaScript can still supply any value.

read more Literal types
Was this clear?
07 How do an annotation, as const, and satisfies differ when controlling literal widening? Mid common reveal ▾ hide ▴

An annotation declares the type a variable or API boundary should expose, so it can intentionally widen a specific initializer to a reusable union. as const asks the checker to preserve literal values and makes object-literal properties readonly and array literals readonly tuples; it does not freeze anything at runtime. satisfies checks that an expression is assignable to a target shape while retaining useful inferred detail instead of replacing the whole expression type with that target. It neither adds readonly behavior nor validates runtime data. I choose among them from the intended contract and mutation model, not by whichever form silences an error.

read more Literal types
Was this clear?
08 What does each built-in TypeScript type guard actually prove? Mid common reveal ▾ hide ▴

typeof narrows JavaScript primitive categories and functions, but object still includes null. instanceof checks a runtime constructor in the prototype chain, so it does not validate plain JSON and can fail across realms. The in operator proves property lookup succeeds on the object or its prototype chain; it says nothing about the property value or ownership. Equality checks prove a particular value relationship, while a literal discriminant narrows an entire union member. I choose the check whose runtime fact matches what the next operation needs, then validate any remaining field constraints separately.

read more Type guards
Was this clear?
09 Why is a custom type predicate a two-way contract? Senior common reveal ▾ hide ▴

A return type such as value is number affects both outcomes: true retains number and false excludes it from the current union. The compiler verifies that number is compatible with the parameter type, but it does not prove the body implements that relationship. A function that returns true only for small numbers therefore cannot safely claim value is number, because a large number reaches the branch the checker may call string. I test valid members, invalid values, and valid members that should return false. If the false side is not “not T,” I return boolean or define a more precise domain type.

read more Type guards
Was this clear?
10 When should you rely on TypeScript inference, and when should you write an annotation? Mid common reveal ▾ hide ▴

I let the compiler infer obvious local values, callback parameters with reliable context, and intermediate results whose shape belongs to one implementation. I annotate boundaries where a change must be reviewed as a contract change: public parameters and returns, empty collections, state whose lifecycle is wider than its initializer, and data entering from untyped code. The test is not whether TypeScript can infer something, but whether implementation edits should be allowed to change what consumers see. For published code, I also compare generated .d.ts output and keep positive and negative type tests.

Was this clear?
11 Why can extracting an inline callback cause an implicit any error? Mid common reveal ▾ hide ▴

An inline callback is checked at a call site whose parameter signature supplies a contextual type. If I first assign the arrow function to an unannotated variable, that function expression is checked as an independent declaration before a later use is considered. The later map, event-registration, or promise call cannot retroactively fill in its parameter types. Under noImplicitAny, the parameter then fails; without it, unsafe any can spread through the body. I either keep a short callback inline or annotate the reusable function with the intended parameter and return contract.

Was this clear?
12 What is the difference between a declared type and an observed type during narrowing? Mid common reveal ▾ hide ▴

The declared type is the assignment contract for the variable across its scope. The observed type is the subset that control-flow facts allow at one program point. A string | number variable may be observed as string after a typeof check, then as number after an assignment, without changing its declaration. At a branch join, TypeScript combines the outcomes of every reachable path. I use this distinction to explain why a later number assignment is legal, while a Boolean assignment still fails, and why an early return can keep the remaining path narrow.

Was this clear?
13 How do discriminated unions and never provide exhaustiveness checking? Mid common reveal ▾ hide ▴

Each union member carries the same literal discriminant, such as status, with a payload specific to that literal. A switch on status narrows the whole object in each case. After every current member is handled, the remaining value has type never, so a default branch can pass it to assertNever. Adding a new member makes the remainder that concrete member instead of never and produces a compile error. This checks the static closed set; data from JSON still needs runtime validation before it may enter the union.

Was this clear?
14 How do union and intersection types differ, and what does that imply for assignment? Mid common reveal ▾ hide ▴

A union A | B accepts a value assignable to at least one member, so an A can flow into A | B. Code reading the union must remain safe for every member until it narrows the value. An intersection A & B accepts only values satisfying both constraints, so it can flow to either A or B. Under TypeScript’s structural typing, these are inclusive relationships: one rich object may satisfy both union members. Neither operator creates or merges runtime data; they describe what the checker accepts and are erased from JavaScript.

Was this clear?
15 What happens when two intersected object types declare the same property incompatibly? Mid occasional reveal ▾ hide ▴

The property requirements are intersected rather than overwritten. Combining { id: string } with { id: number } therefore gives an id whose type is string & number, effectively never for ordinary values. The & operator does not follow JavaScript object-spread order and cannot choose a winning field. I inspect overlapping keys before composition. If the domain replaces the field, I use Omit on the old shape and add the new declaration, then perform any runtime conversion explicitly. If the fields mean different things, I rename them instead of suppressing the conflict with an assertion.

Was this clear?
52 When is a const type parameter useful for literal inference? Mid occasional reveal ▾ hide ▴

In TypeScript 6, a const type parameter asks inference to prefer an as const-like candidate for object, array, and primitive literals written directly at the call site. It suits tuple factories, route definitions, and event tables where the written structure is part of the contract. It does not freeze the runtime value, and it cannot recover literal information from a variable already widened to string or number[]. The trade-off is that very narrow readonly results can make mutation awkward, so ordinary type parameters are better when callers need a broad, mutable contract.

read more Literal types
Was this clear?
56 How does an assertion function differ from a type predicate? Mid occasional reveal ▾ hide ▴

In TypeScript 6, a predicate such as value is User returns a Boolean and narrows both the true and false branches. An assertion function such as asserts value is User promises that any normal return establishes the condition, so following code is narrowed without an if. Its failure path must therefore throw or otherwise terminate. Logging an error and returning is an unsound concrete pitfall. I use predicates for recoverable branching and assertions for fatal configuration or invariant failures. When callers need several validation errors, a discriminated result is more informative than either form.

read more Type guards
Was this clear?
57 Why should an empty collection or initial null often have an annotation? Mid occasional reveal ▾ hide ▴

In TypeScript 6, an empty literal provides little evidence: an empty array has no element candidates, {} does not acquire declared properties from later writes, and null cannot describe a future Connection. Strict control-flow analysis may evolve some local array observations after push, but that is not a stable exported contract. I annotate the intended element type or full lifecycle, such as Job[] or Connection | null, at the ownership boundary. The trade-off is slightly more syntax, but broad assertions like { } as Config merely hide partially initialized states and move errors to distant consumers.

Was this clear?
58 When does TypeScript preserve narrowing inside a closure? Senior occasional reveal ▾ hide ▴

TypeScript 6 can preserve a narrowed parameter or let variable in a non-hoisted closure created after a definite last assignment, provided no nested function assigns that variable. If another closure can write it—even assigning it to itself—the checker abandons the old fact because invocation order is unknown. I prefer validating and normalizing first, then capturing a const such as normalizedUrl; this makes the proof boundary obvious. The compiler rule is not a concurrency guarantee: after await, shared mutable objects may still change through aliases, so ownership or copying remains necessary.

Was this clear?
59 Why is a discriminated union safer than several independent union fields? Mid common reveal ▾ hide ▴

In TypeScript 6, { format: "json"; payload: string } | { format: "binary"; payload: Uint8Array } preserves a relationship: checking format narrows the matching payload. Replacing it with { format: "json" | "binary"; payload: string | Uint8Array } forms two independent choices and admits invalid pairings. Several optional fields have the same problem and make illegal states representable. A discriminated union costs more explicit variants, but enables exhaustiveness and clearer construction. It remains a static contract, so JSON must still be validated by discriminant and corresponding payload before entering the union.

Was this clear?

Generics and advanced types

17 questions
16 How do keyof, indexed access, and mapped types work together? Mid common reveal ▾ hide ▴

keyof T produces the union of known keys in T. Indexed access, T[K], retrieves the value type associated with one key or a union of keys. A mapped type iterates over a key union and constructs one property per key, optionally changing modifiers, values, or names. A common pattern maps each key to a complete object and then indexes the mapped object with keyof T to obtain a discriminated union. Keeping the same K in related fields preserves their correlation; indexing the fields independently loses it.

Was this clear?
17 Why can a precisely derived TypeScript type still be unsafe at a data boundary? Mid common reveal ▾ hide ▴

TypeScript erases types when it emits JavaScript, so a conditional or mapped type performs no runtime validation. JSON.parse(raw) as Event merely tells the checker to trust the programmer; it does not inspect a discriminant, required field, or numeric range. External values should enter as unknown and pass through a runtime parser or validator before receiving the domain type. After that boundary, derived types are valuable because they preserve relationships among trusted values. I also audit any assertion connecting Object.keys or dynamic assignment to a mapped type, since runtime enumeration and keyof are not identical.

Was this clear?
18 What does a generic type parameter express that any does not? Mid common reveal ▾ hide ▴

A type parameter connects positions in a signature for one call or one instance. In (value: T) => T, the caller learns that the result preserves the selected input type. any accepts the input but opts out of checking, so the result carries no trustworthy relationship and unsafe operations can spread. I use unknown when a value may have any runtime shape but must be narrowed before use. I use a generic when two or more positions must vary together, such as an array element, callback parameter, and optional result.

read more Generics
Was this clear?
19 When should a type parameter belong to a method versus its interface or class? Mid occasional reveal ▾ hide ▴

Put the parameter on a method when every call may choose independently, as with a converter that accepts a new input type each time. Put it on an interface or class when one choice must govern several members for the lifetime of an instance, as with Repository or Map<Key, Value>. This placement records who owns the choice and how long it lasts. I also check static members separately because they belong to the constructor, not a generic instance, so a static factory must declare type parameters of its own.

read more Generics
Was this clear?
20 How do you choose between Pick, Omit, and a separately declared interface? Mid common reveal ▾ hide ▴

I choose based on how source changes should propagate. Pick is an allowlist, so new source fields stay out; that is usually right for public responses and restricted commands. Omit is a denylist, so new source fields flow in automatically; that can suit internal shapes that differ only by infrastructure fields. A separate interface is better when two contracts merely look similar but should evolve independently. I also remember that neither utility changes runtime data. For security-sensitive output I construct an allowlisted object and test its serialized form, regardless of the static type I expose.

read more Utility types
Was this clear?
21 Why is Partial<Entity> usually a poor update API? Mid common reveal ▾ hide ▴

Partial changes whether every top-level property may be absent; it does not decide which fields a caller is authorized to update. Applied to a persistence entity, it often exposes IDs, ownership, roles, audit fields, and secrets. It is also shallow, so a nested object is replaced as a whole rather than becoming a well-defined nested patch. I start with Pick over an explicit update allowlist, then apply Partial or declare operation-specific optional fields. At runtime I reject unknown keys, define absence versus clearing, and test under exactOptionalPropertyTypes so explicit undefined cannot silently change the protocol.

read more Utility types
Was this clear?
22 When does a conditional type distribute over a union, and how do you test the union as a whole? Senior common reveal ▾ hide ▴

Distribution occurs when the checked side is a naked type parameter, as in T extends U ? X : Y. If T is a union, TypeScript applies the conditional to each member and unions the results. Thus ToArray<string | number> can become string[] | number[], not (string | number)[]. To make one comparison against the complete union, wrap both sides: [T] extends [U]. I test both versions with a mixed union and with never, because distribution over never produces never without evaluating an ordinary member branch.

read more
Was this clear?
23 What must a production DeepPartial utility decide before it recurses? Senior rare reveal ▾ hide ▴

It must define which shapes are records to traverse and which are atomic values to preserve. A naive T extends object branch also matches functions, arrays, tuples, Date, and class instances, often destroying call signatures or collection semantics. I handle primitives and functions first, decide whether tuples and arrays need separate branches, and document treatment of built-ins. I also distinguish an optional property from a required property whose value includes undefined, especially under exactOptionalPropertyTypes. Finally, I bound or simplify recursion and test unions, readonly members, and already-optional fields so compiler cost and output remain predictable.

read more
Was this clear?
24 What does infer do inside a conditional type, and what happens when the pattern does not match? Senior common reveal ▾ hide ▴

infer introduces a type variable at a position inside the extends pattern. When the input is assignable to that pattern, TypeScript captures the corresponding part and makes the variable available in the true branch. For example, T extends readonly (infer E)[] ? E : never extracts an array or tuple element type. If the pattern does not match, the false branch runs; infer is not runtime reflection and the binding does not escape its branch. For tuples I use rest patterns to preserve positions, and for overloaded functions I remember that inference uses the final call signature rather than resolving every overload.

read more
Was this clear?
25 How do key remapping and never filter properties in a mapped type? Senior common reveal ▾ hide ▴

A mapped type iterates over a key union, usually keyof T, and emits one property for each key. The as clause computes the output key. If that computation produces never, the property is omitted; otherwise it may preserve or transform the name, often with a template literal type. The value side still uses the current key, such as T[K], so filtering by value type can keep names and values correlated. Modifier prefixes like -? and -readonly change optionality and mutability independently. This is a compile-time transformation only: it neither renames nor removes properties from a JavaScript object at runtime.

read more
Was this clear?
26 How do you keep a recursive conditional type correct and tractable? Senior rare reveal ▾ hide ▴

Start with a real base case and make every recursive branch structurally smaller, such as peeling one tuple element or one array layer. Decide explicitly whether unions should distribute and whether functions, arrays, tuples, and built-in objects are leaves or containers. Unbounded object recursion can expand huge unions, lose tuple detail, or trigger an excessively deep instantiation error. For public utilities I often add a depth accumulator and return a documented fallback when the limit is reached. I test empty tuples, readonly collections, unions, never, and self-referential object types. Runtime cyclic data is a separate problem and needs cycle-aware traversal.

read more
Was this clear?
27 How do template literal types derive string APIs, and where do they stop being useful? Senior occasional reveal ▾ hide ▴

A template literal type concatenates literal components at compile time. If a placeholder is a union, TypeScript forms the cross-product, so ${Locale}_${Message} derives every allowed pair. Combined with key remapping and Capitalize, it can derive names such as onUserCreated from one source union. This is useful when the set is finite and owned by the program. Large unions multiply quickly, slowing checking and producing unreadable diagnostics; open-ended user strings gain little precision. The type also performs no runtime parsing, so routes, event names, and environment keys from external sources still require validation.

read more
Was this clear?
28 How can you tell whether a generic type parameter is carrying useful information? Mid common reveal ▾ hide ▴

A useful type parameter relates at least two positions or constrains one position in a way callers can observe. In <T>(value: T) => T, it connects input and output; in <K extends keyof T>, it connects a selected key with T[K]. A parameter used only once often should be a concrete type or unknown, because callers gain no relationship from choosing it. Constraints describe required capabilities but do not convert values or validate runtime data. I prefer inference at call sites, add explicit type arguments only when inference lacks evidence, and inspect the emitted declaration to ensure defaults and constraints do not widen the public contract.

read more
Was this clear?
29 How do you preserve the relationship between an event name and its payload type? Senior occasional reveal ▾ hide ▴

Start with one event map, such as { saved: Saved; failed: Failure }, and make the event name a type parameter: K extends keyof Events. The payload position then uses Events[K], so each call selects one key and its matching value type together. Writing keyof Events and Events[keyof Events] independently loses that correlation and permits a valid payload for the wrong event. The same map can derive handler properties with a mapped type or a discriminated union for queues. At runtime, dynamic names and external payloads still need validation; the generic only preserves relationships among values already accepted by the checker.

read more
Was this clear?
46 How would you keep a public recursive or conditional type tractable? Senior occasional reveal ▾ hide ▴

In TypeScript 6, I first define the supported input shapes and real base cases, then make every recursive branch structurally smaller. I test the final exported alias—not only its helpers—with positive assignments, @ts-expect-error negatives, unions, never, and the deepest supported shape. If diagnostics expose layers of distribution or compilation reaches excessive instantiation, I name intermediate types, split stages, or return a named domain type. A depth parameter can bound work, but its cutoff is a design trade-off, not a stable compiler limit across versions.

Was this clear?
51 Why does a generic function need a runtime witness for some operations? Mid common reveal ▾ hide ▴

Type parameters are erased in TypeScript 6, so a function cannot evaluate value instanceof T, call new T(), or select a serializer by inspecting T. It must receive runtime evidence: a constructor for creation, a predicate or schema for validation, or a tag or strategy for dispatch. The generic parameter then connects that evidence to the promised result. A concrete pitfall is accepting (unknown) => value is T and assuming it is truthful—the checker does not verify its body. Test the witness independently, and prefer a simpler non-generic API when no useful type relationship remains.

read more Generics
Was this clear?
60 When does Record model a complete lookup, and when can a key still be missing? Mid occasional reveal ▾ hide ▴

In TypeScript 6, Record<ClosedKeyUnion, Handler> requires every known key and supports precise reads after the index is narrowed to that union. Record<string, Handler> instead describes a string index signature; it does not create every possible property at runtime. With noUncheckedIndexedAccess, an undeclared string lookup gains undefined, exposing that gap. That option is outside the strict umbrella and must be enabled deliberately. I use a finite key union for owned registries and a checked lookup or Map for open keys. The pitfall is calling a missing handler because the broad static type implied certainty.

read more Utility types
Was this clear?

Config and migration

12 questions
30 What does a declaration file guarantee, and what must be verified separately? Mid common reveal ▾ hide ▴

A declaration file gives the TypeScript checker a static contract for code implemented elsewhere. It can reject calls with incompatible arguments, expose documented members to editors, and preserve relationships expressed by its types. It does not create a function, validate external data, or prove that JavaScript exports the declared value. I verify both halves: run type tests that cover accepted and rejected calls, then import the packed runtime artifact and exercise the same public entry. A declaration is useful only while its names, module format, optional results, and asynchronous behavior match the implementation.

Was this clear?
31 How would you test a handwritten declaration against an untyped JavaScript package? Senior common reveal ▾ hide ▴

I start from the packed package rather than its source tree, because file lists and exports are part of the contract. Positive tests import every public entry and check representative inference. Negative tests use @ts-expect-error for arguments and results that must be rejected; an unused directive then catches accidental widening. I run tsc with the consumer configurations the package supports and inspect traceResolution when paths differ. Finally, I execute runtime imports for the same scenarios and compare export shape, missing-value behavior, promises, and thrown errors. Type tests and runtime tests cover different failure surfaces, so neither replaces the other.

Was this clear?
32 What does TypeScript strict mode guarantee, and where does that guarantee stop? Mid common reveal ▾ hide ▴

Strict mode enables a family of conservative static checks for nulls, implicit any, this, function compatibility, class initialization, catch variables, built-in iterators, and bind/call/apply. It rejects source operations that lack enough type evidence. The guarantee stops at code the compiler can check. Types are erased from emitted JavaScript, so JSON, network responses, JavaScript callers, explicit any, assertions, and suppressed diagnostics can still violate the declared model. I treat strict mode as a strong default for trusted typed code and add runtime validation at every untrusted boundary.

read more Strict mode
Was this clear?
33 How does strictFunctionTypes affect callback compatibility, and what exception matters? Senior common reveal ▾ hide ▴

For function properties, parameter compatibility is checked contravariantly. A handler accepting Animal can stand in for one that will receive only Dog, because it can handle every value that caller supplies. A dog-only handler cannot stand in for a general Animal handler. Method syntax remains bivariant for compatibility with common class and DOM hierarchies, so an interface method may admit the unsafe direction. I prefer function-property syntax for new callback contracts and test existing method-based APIs with wider inputs. Return types follow the ordinary covariant direction and should be reviewed separately.

read more Strict mode
Was this clear?
34 What does TypeScript type coverage measure, and what is its denominator? Mid common reveal ▾ hide ▴

The type-coverage tool counts identifiers, not lines. Its basic ratio is identifiers whose checker type is not any divided by all identifiers in the selected program. One upstream any can therefore create several uncovered positions as it flows through properties, calls, and callbacks. The denominator depends on the effective tsconfig, file filters, allowJs, generated sources, compiler version, and tool version. I only compare results when those inputs are fixed, and I inspect raw counts and detail output because a rounded percentage can hide a small regression.

read more Type coverage
Was this clear?
35 Why does 100% type coverage not prove runtime type safety? Mid common reveal ▾ hide ▴

Coverage reports what the checker believes, not whether that belief matches runtime data. An inaccurate interface, handwritten declaration file, non-null assertion, or JSON.parse(raw) as User can give identifiers concrete types without checking a single incoming value. Types are then erased from emitted JavaScript. I treat a perfect score as evidence that current rules found no tracked escape hatch, not as a soundness proof. External values still enter as unknown, pass runtime validation, and receive boundary tests for missing fields, wrong primitives, malformed containers, and domain constraints.

read more Type coverage
Was this clear?
36 How would you migrate a large JavaScript codebase to strict TypeScript without freezing delivery? Senior common reveal ▾ hide ▴

I first establish one reproducible build and test baseline, then let JavaScript and TypeScript coexist with allowJs; checkJs or JSDoc can expose risk before files are renamed. I migrate dependency leaves and stable boundaries in small changes, type untrusted inputs as unknown, and place narrow adapters around legacy modules. Each converted area gets a stricter configuration or error budget that can only ratchet upward. I track explicit any, assertions, suppressed diagnostics, and unchecked boundaries rather than counting .ts files alone. Continuous tests and emitted-artifact checks keep runtime behavior stable while compiler guarantees increase.

read more
Was this clear?
37 What do TypeScript project references change about a monorepo build? Senior occasional reveal ▾ hide ▴

References turn one large program into an explicit graph of composite projects. A consuming project type-checks against a dependency’s declaration output instead of loading all dependency source into the same program. Running tsc -b follows the graph, builds prerequisites in dependency order, and uses build information to skip work that is up to date. A solution config commonly has files: [] plus only references. Each referenced project must satisfy composite-project file and declaration rules. References describe TypeScript build dependencies, not runtime package resolution, so workspace manifests, exports, and emitted paths still need separate alignment and tests.

read more
Was this clear?
38 How should module and moduleResolution be chosen in tsconfig.json? Mid common reveal ▾ hide ▴

Choose them from the runtime and build pipeline, not from a preferred syntax. module controls the module form TypeScript preserves or emits, while moduleResolution controls how specifiers, package exports, extensions, and type declarations are found. A Node application should use the matching Node mode and package settings; code handed to a bundler should use the supported bundler-oriented mode. target and lib are separate decisions about emitted syntax and available global types. I verify with tsc --showConfig, resolution tracing when needed, and an execution test of the emitted or bundled artifact, because successful type checking alone does not prove the runtime loader agrees.

read more
Was this clear?
49 Why do .d.mts, .d.cts, and package exports need to match runtime entries? Senior occasional reveal ▾ hide ▴

Under TypeScript 6 Node-style resolution, .d.mts describes an ESM .mjs entry and .d.cts describes a CommonJS .cjs entry; plain .d.ts follows the surrounding package format for .js. Every exported subpath and condition must lead consumers to declarations that describe the corresponding runtime file. Copying one declaration to every target can hide default-export or export = mismatches. I test the packed artifact from minimal ESM and CommonJS consumers. The trade-off is duplicated build plumbing, but it prevents source-tree aliases and interop flags from masking a broken publication.

Was this clear?
54 Why is strict true not the same as maximum TypeScript hardening? Mid occasional reveal ▾ hide ▴

The strict umbrella evolves: TypeScript 6 includes strictBuiltinIteratorReturn, so upgrading can add diagnostics without a config edit. It still does not automatically enable noUncheckedIndexedAccess, exactOptionalPropertyTypes, or noImplicitOverride; each protects a different contract and can require migration work. A child config may also override one strict option. I verify the effective setup with tsc --showConfig -p ... and run CI through the same project entry. The pitfall is silencing new findings with assertions, which restores a green build while discarding the intended protection.

read more Strict mode
Was this clear?
55 How would you make type coverage a stable CI ratchet? Mid occasional reveal ▾ hide ▴

I pin TypeScript 6, the type-coverage version, effective tsconfig, strict-counting mode, and included file set, then record both numerator and denominator at the baseline. CI blocks decreases with --at-least; stabilized scopes may use exact --is. Raw counts and detail diffs catch regressions hidden by rounded percentages, while reviewed exclusions stay version-controlled. When the compiler or tool changes, I recalculate the baseline in a dedicated change. The trade-off is maintenance during upgrades, but mixing a counting-policy change with product code makes the historical trend meaningless and encourages symptom-fixing annotations instead of repairing the first any source.

read more Type coverage
Was this clear?

Patterns

9 questions
39 What guarantee does a TypeScript branded type provide, and what does it not provide? Mid common reveal ▾ hide ▴

A branded type makes structurally identical values incompatible by intersecting a base type with a marker. If UserId and ProductId have distinct brands, checked TypeScript cannot pass one where the other is required, while either can still be used as its base type. The guarantee is static and depends on controlled construction. The marker is erased from JavaScript, so it performs no validation, existence check, or authorization. Assertions, any, and JavaScript callers can bypass it. I therefore describe a brand as evidence that one named constructor checked a local invariant, not as proof of every business fact about the value.

read more Branded types
Was this clear?
40 How would you design a safe constructor for a branded value from external data? Senior common reveal ▾ hide ▴

I accept unknown at the trust boundary and write down the complete invariant before coding. For a positive numeric ID, that usually means checking the primitive type, integrality, positivity, and safe-integer range. Only the success branch contains the assertion that returns the branded type; callers receive a useful error, Result, or exception on failure. I export the branded alias and constructor together but keep the unique-symbol key private. I avoid a public generic cast helper because it creates unchecked values. Tests cover valid data, missing or wrong types, fractions, bounds, and every other condition named by the contract.

read more Branded types
Was this clear?
41 How does satisfies differ from a type annotation? Mid common reveal ▾ hide ▴

A type annotation fixes the type exposed by a variable, so reads and later assignments use that written contract. The satisfies operator instead checks whether an expression is assignable to a target and keeps the expression’s contextually inferred result. That is useful for a registry where I want complete keys and valid entries while still deriving precise names or discriminants. I choose an annotation when the variable must evolve across the target’s full range or when an exported API needs a stable public type. Neither form performs runtime validation.

Was this clear?
42 Does satisfies preserve every literal type without affecting inference? Senior common reveal ▾ hide ▴

No. The target participates in contextual typing before assignability is checked. A property targeted by the union “GET” | “POST” can retain “GET”, while one targeted by plain string usually widens to string; numeric object properties also normally widen. A tuple branch in the target can make an array literal infer as a tuple. After this contextual inference, satisfies does not replace the whole result with the target type or add omitted optional properties. I verify the compiler’s displayed type and write positive type tests instead of relying on the slogan that it preserves inference.

Was this clear?
43 How does a standard TypeScript method decorator change behavior without changing the declared API? Senior occasional reveal ▾ hide ▴

A standard method decorator receives the original method value and a context object, then may return a compatible replacement. A safe wrapper preserves the original this, parameter tuple, return type, thrown errors, and asynchronous behavior; it calls the original with apply or call rather than detaching it. The context supplies facts such as the member name and whether it is static or private, while addInitializer schedules instance or class setup. The type signature cannot advertise new instance members merely because runtime code adds them. Standard decorators also differ from the legacy experimentalDecorators and metadata model, so libraries must state which contract they require.

read more
Was this clear?
44 What must be true for a module augmentation to be sound at runtime? Senior rare reveal ▾ hide ▴

The augmented module specifier must resolve to the same module as the original declaration, and the added type members must describe behavior that actually exists at runtime. Augmentation merges declarations; it does not install a prototype method or execute a plugin. If a side-effect import performs the patch, that import must run before callers use the member. An augmentation may patch existing declarations but cannot add a new top-level export, and a default export cannot be targeted by name. I keep the file in the compilation, import the original module, and test both type checking and the runtime installation order.

read more
Was this clear?
45 In what order do multiple decorator factories run, and why does that matter? Senior occasional reveal ▾ hide ▴

For decorators written top to bottom as @first() and @second(), the factory expressions are evaluated top to bottom, but the resulting decorators are applied bottom to top. The shape therefore resembles function composition: first(second(method)). Order matters whenever wrappers log, cache, authorize, retry, or translate errors, because the outer wrapper observes a different call and result from the inner one. I avoid depending on decoration-time side effects, document the intended stack, and test one real call including this, arguments, return value, rejection, and thrown error. I also confirm whether the code uses standard decorators or the legacy experimentalDecorators signatures.

read more
Was this clear?
48 When must a branded value be revalidated after a transformation? Senior occasional reveal ▾ hide ▴

TypeScript 6 models a brand as an intersection, so the branded value retains its base type’s operations. Those operations do not automatically preserve the proof: arithmetic on a branded number produces an ordinary number, serialization loses the brand, and a wrapper declared to return the base type widens it. I re-run the named constructor whenever an operation can violate range, format, precision, or a newer validation rule. Identity-style generics may preserve the brand statically, but that signature is trustworthy only if runtime behavior is unchanged. Stateful multi-field invariants often deserve an encapsulated object instead.

read more Branded types
Was this clear?
53 How would you verify the contract of a registry declared with satisfies? Senior occasional reveal ▾ hide ▴

With TypeScript 6, I run type tests under the project’s real tsconfig, not only inspect an editor hover. Positive cases consume retained details, such as a specific method literal or keyof typeof registry. Negative cases use @ts-expect-error for a missing key, extra key, invalid value, and forbidden reassignment. I also inspect emitted declarations when the registry is exported, because narrow implementation details can become public API. These checks cover compiler behavior only; dynamic names still need runtime parsing. A broad Record<string, ...> target is a pitfall because it silently abandons finite-key completeness.

Was this clear?