JavaScript Interop
Living with untyped JavaScript — JSDoc types and @ts-check in .js files, libraries with no @types, declaring globals added by a script tag, typing Proxy-based APIs, and prototype-based code that predates classes.
8 questions
JuniorTheoryVery commonA JavaScript library ships no types and no @types package. What are your options?
A JavaScript library ships no types and no @types package. What are your options?
Write a local declaration file: put declare module 'lib' in a .d.ts inside your project and describe only the exports you actually use. A bodyless declare module 'lib'; compiles too, but types the whole import as any.
Common mistakes
- ✗Expecting the compiler to infer types from a dependency's JavaScript source automatically
- ✗Thinking a bodyless
declare modulegives real types — it only silences the error and yieldsany - ✗Believing a locally written
.d.tscannot describe a package that lives undernode_modules
Follow-up questions
- →How does the compiler decide which
.d.tsfile applies to an import oflib? - →When would you publish the declarations to DefinitelyTyped instead of keeping them local?
JuniorTheoryCommonHow does a declare inside a module differ from one in a global script file?
How does a declare inside a module differ from one in a global script file?
A file with a top-level import or export is a module, so everything it declares is scoped to it. A file with neither is a global script, and its declare lands in the global scope. From a module, only declare global { ... } reaches global.
Common mistakes
- ✗Thinking
declareis inherently global, no matter whether the file is a module - ✗Forgetting that a single
exportturns a.d.tsinto a module, so its declares stop being global - ✗Believing
declareemits a runtime binding — it is type-only and erased entirely
Follow-up questions
- →Your
.d.tsstopped taking effect after you added animportto it. Why? - →When do you use
declare globalversus augmenting one specific module?
JuniorCodeCommonTyping a plain-JavaScript function with JSDoc so that tsc checks it
Typing a plain-JavaScript function with JSDoc so that tsc checks it
Declare the shape once with @typedef, then annotate the function with @param {Type} name and @returns. Under @ts-check the compiler reads those tags as real types, so formatUser({ name: 42 }, 'yes') fails to build — no rename needed.
Common mistakes
- ✗Thinking JSDoc tags are documentation only and cannot type-check a
.jsfile - ✗Writing TypeScript annotation syntax inside a
.jsfile, which is a parse error - ✗Forgetting
@ts-checkorcheckJs, so the tags are parsed but nothing is enforced
Follow-up questions
- →How do you express a generic function in JSDoc, and which tag does that take?
- →What can a
.tsfile express that JSDoc inside a.jsfile cannot?
MiddleCodeCommonTyping a global that a third-party <script> tag adds to window
Typing a global that a third-party <script> tag adds to window
Augment Window by declaration merging: declare global { interface Window { analytics: Analytics } }. A module needs the declare global wrapper to reach global scope. Merging adds the member to the existing Window, so no assertion is needed.
Common mistakes
- ✗Omitting
declare global, so the augmentation stays inside the module and the call site still errors - ✗Not realising that a single
importorexportturns the.d.tsinto a module - ✗Reaching for an assertion at each call site instead of merging the member into
Windowonce
Follow-up questions
- →Should
analyticsbe optional, given that the vendor script may fail to load? - →How would you instead type a global that is only ever present in tests?
MiddleTheoryOccasionalHow do you type a JS library built on arguments and prototype assignment?
How do you type a JS library built on arguments and prototype assignment?
Type the contract, not the implementation. An arguments-driven function becomes an overload set — one signature per calling convention — because the compiler checks call sites, never the body. A prototype-built constructor becomes a declare class.
Common mistakes
- ✗Mirroring the runtime object graph in the
.d.tsinstead of describing the public contract - ✗Reaching for an
any[]rest parameter where an overload set expresses the real calling conventions - ✗Assuming a declaration file constrains the library's implementation — it constrains only its callers
Follow-up questions
- →How would you express a function that behaves differently when it is called with
new? - →What goes wrong when a hand-written
.d.tsdrifts from the library's real behaviour?
MiddleCodeOccasionalTyping a Proxy-based SDK whose members do not exist at run time
Typing a Proxy-based SDK whose members do not exist at run time
Declare the shape the proxy only pretends to have, then assert once in the factory: return new Proxy({} as Api, handler). A get trap returns any, so the compiler can never verify the synthesized members. One assertion keeps callers fully typed.
Common mistakes
- ✗Expecting the compiler to derive the type from the
gettrap, which returnsany - ✗Spreading assertions across every call site instead of confining one to the factory
- ✗Believing a
gettrap fires only for keys that already exist on the proxy target
Follow-up questions
- →How would you generate the
Apiinterface from a route table with a mapped type? - →What breaks if the proxy is missing an endpoint that the interface promises?
MiddleTheoryOccasionalWhat does @ts-check with JSDoc give you, and what do you still lose versus .ts?
What does @ts-check with JSDoc give you, and what do you still lose versus .ts?
You get the real checker on a .js file: inference, narrowing and JSDoc types are all enforced, with no rename and no build step. What you lose is the syntax — every type lives in a comment, and enum, decorators and parameter properties have no JSDoc form.
Common mistakes
- ✗Thinking
@ts-checkis an editor-only feature thattscand CI ignore - ✗Assuming JSDoc reaches full parity with the
.tssyntax - ✗Expecting a JSDoc-typed
.jsfile to be checked withoutcheckJsor a per-file@ts-check
Follow-up questions
- →How do you write a type assertion in JSDoc, and why does it need the parentheses?
- →At what point in a migration does JSDoc stop paying for itself?
SeniorTheoryOccasionalYour class must extend a prototype-based constructor from an untyped JS library. What breaks?
Your class must extend a prototype-based constructor from an untyped JS library. What breaks?
extends needs a value the compiler knows is newable, so the base needs a declare class. That declaration is an unchecked promise: nothing verifies it against the library, and drift surfaces as a crash rather than a compile error.
Common mistakes
- ✗Assuming a
declare classis checked against the library's real implementation - ✗Expecting the compiler to walk the prototype chain — it only ever sees the declared shape
- ✗Thinking an ES2015 class cannot extend a function-based constructor at all
Follow-up questions
- →Why does emitting to the
ES5target breakextendsof a built-in such asErrororArray? - →How would you type a factory that is callable both with and without
new?