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.
Basics
7 questions · 0 Seen01 When should you write a TypeScript annotation instead of relying on inference? 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.
02 How do unknown and any differ at an external data boundary? 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.
03 What JavaScript does a TypeScript enum create, and how do numeric and string enums differ? 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.
04 When would you choose an enum, an as const object, or a literal union? 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.
05 How do ES modules and TypeScript namespaces differ, and which should organize application code? 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.
47 How do structural typing and excess-property checks differ in TypeScript? 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.
50 Why is publishing an ambient const enum risky across package versions? 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.
Type system
15 questions · 0 Seen06 When should you use a literal union instead of string? 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.
07 How do an annotation, as const, and satisfies differ when controlling literal widening? 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.
08 What does each built-in TypeScript type guard actually prove? 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.
09 Why is a custom type predicate a two-way contract? 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.
10 When should you rely on TypeScript inference, and when should you write an annotation? 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.
11 Why can extracting an inline callback cause an implicit any error? 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.
12 What is the difference between a declared type and an observed type during narrowing? 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.
13 How do discriminated unions and never provide exhaustiveness checking? 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.
14 How do union and intersection types differ, and what does that imply for assignment? 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.
15 What happens when two intersected object types declare the same property incompatibly? 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.
52 When is a const type parameter useful for literal inference? 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.
56 How does an assertion function differ from a type predicate? 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.
57 Why should an empty collection or initial null often have an annotation? 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.
58 When does TypeScript preserve narrowing inside a closure? 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.
59 Why is a discriminated union safer than several independent union fields? 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.
Generics and advanced types
17 questions · 0 Seen16 How do keyof, indexed access, and mapped types work together? 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.
17 Why can a precisely derived TypeScript type still be unsafe at a data boundary? 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.
18 What does a generic type parameter express that any does not? reveal ▾ hide ▴
A type parameter connects positions in a signature for one call or one instance. In
19 When should a type parameter belong to a method versus its interface or class? 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
20 How do you choose between Pick, Omit, and a separately declared interface? 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.
21 Why is Partial<Entity> usually a poor update API? 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.
22 When does a conditional type distribute over a union, and how do you test the union as a whole? 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.
23 What must a production DeepPartial utility decide before it recurses? 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.
24 What does infer do inside a conditional type, and what happens when the pattern does not match? 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.
25 How do key remapping and never filter properties in a mapped type? 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.
26 How do you keep a recursive conditional type correct and tractable? 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.
27 How do template literal types derive string APIs, and where do they stop being useful? 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.
28 How can you tell whether a generic type parameter is carrying useful information? 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.
29 How do you preserve the relationship between an event name and its payload type? 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.
46 How would you keep a public recursive or conditional type tractable? 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.
51 Why does a generic function need a runtime witness for some operations? 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.
60 When does Record model a complete lookup, and when can a key still be missing? 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.
Config and migration
12 questions · 0 Seen30 What does a declaration file guarantee, and what must be verified separately? 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.
31 How would you test a handwritten declaration against an untyped JavaScript package? 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.
32 What does TypeScript strict mode guarantee, and where does that guarantee stop? 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.
33 How does strictFunctionTypes affect callback compatibility, and what exception matters? 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.
34 What does TypeScript type coverage measure, and what is its denominator? 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.
35 Why does 100% type coverage not prove runtime type safety? 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.
36 How would you migrate a large JavaScript codebase to strict TypeScript without freezing delivery? 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.
37 What do TypeScript project references change about a monorepo build? 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.
38 How should module and moduleResolution be chosen in tsconfig.json? 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.
49 Why do .d.mts, .d.cts, and package exports need to match runtime entries? 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.
54 Why is strict true not the same as maximum TypeScript hardening? 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.
55 How would you make type coverage a stable CI ratchet? 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.
Patterns
9 questions · 0 Seen39 What guarantee does a TypeScript branded type provide, and what does it not provide? 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.
40 How would you design a safe constructor for a branded value from external data? 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.
41 How does satisfies differ from a type annotation? 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.
42 Does satisfies preserve every literal type without affecting inference? 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.
43 How does a standard TypeScript method decorator change behavior without changing the declared API? 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.
44 What must be true for a module augmentation to be sound at runtime? 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.
45 In what order do multiple decorator factories run, and why does that matter? 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.
48 When must a branded value be revalidated after a transformation? 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.
53 How would you verify the contract of a registry declared with satisfies? 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.
No questions match this filter.