Typorithm · medium

14. Declarations and gradual JavaScript adoption

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

14. Declarations and gradual JavaScript adoption

0%

Concept and contract

Treat declaration files as external contracts, not as generated runtime implementations.

A declaration file describes exports that exist elsewhere. It contributes checking information but no executable implementation. Consuming an inaccurate declaration can therefore produce code that compiles and still fails immediately when the real package behaves differently.

Prefer a package’s maintained bundled declarations when available. Use matching community definitions or a small reviewed local declaration when necessary. Avoid declare module with an unrestricted any export as a permanent integration strategy, because it removes useful checking across the entire dependency boundary.

Mechanism and worked example

The example defines an adapter around a JavaScript-style unknown result.

The example defines an adapter around a JavaScript-style unknown result. A narrow validator converts that uncertain value into a local trusted contract. This is especially useful when adopting legacy modules incrementally: the adapter contains uncertainty instead of spreading assertions through every consumer.

allowJs includes JavaScript files in the project; checkJs requests checking for them, and JSDoc can supply contracts. These settings do not magically recover every intended invariant. Begin with one module, keep runtime regression tests and migrate exported contracts before changing all internal syntax.

Complete worked example

typescript

type User = { id: string };
function adapt(value: unknown): User | undefined {
  return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string"
    ? { id: value.id } : undefined;
}
Reasoning
The adapter establishes a trusted local shape instead of asserting that external output already matches it.

Counterexamples and common mistakes

The rejected declaration claims a string result can be assigned to a number.

The rejected declaration claims a string result can be assigned to a number. That catches a static misuse only if the declaration itself is truthful. Declaring the result any would let both assignments through and conceal potential integration failures.

Do not add declare to a missing runtime function merely to satisfy the compiler. The resulting JavaScript will still try to call something that does not exist. A declaration should describe a verified external export or a real runtime global supplied by the host.

Rejected counterexample: expected compiler diagnostic

typescript

declare function externalName(): string;
// @ts-expect-error The declared contract returns text.
const size: number = externalName();
Reasoning
A declaration can restrict callers without supplying any runtime implementation.

Independent practice

Wrap an external lookup that returns unknown.

Wrap an external lookup that returns unknown. Accept a result only when it contains a string id and boolean active property. Return undefined for every malformed result. Keep the external function injectable so tests can supply valid and invalid responses without a real network dependency.

Test a valid inactive record, because false must not be mistaken for absence. Also test null and a string active value. Document whether extra properties are ignored or rejected; this adapter intentionally projects only the two fields the local consumer needs.

Step 1: Call the injected lookup once. Step 2: Validate and project id and active.

Practice starter: solve before reading the solution

typescript

type Account = { id: string; active: boolean };
function lookupAccount(lookup: () => unknown): Account | undefined { throw new Error("TODO"); }
Reasoning
Inject uncertain output and test false as a valid boolean value.
  • Hint 1: False is a valid active value.
  • Hint 2: A declaration does not validate external output.

Solution, tests and trade-offs

The solution checks both primitive fields and constructs a fresh local record.

The solution checks both primitive fields and constructs a fresh local record. That projection separates external evolution from internal assumptions. Injecting the lookup also makes its runtime contract testable instead of relying on a handwritten declaration as the only evidence.

Run adapter tests against known external fixtures when integrating a real package. Compile-time checks verify consumer expectations; contract tests detect mismatches between declarations and actual runtime exports. Keep these responsibilities distinct when diagnosing an interoperability bug.

Practice solution and acceptance checks

typescript

type Account = { id: string; active: boolean };
function lookupAccount(lookup: () => unknown): Account | undefined {
  const value = lookup();
  if (typeof value !== "object" || value === null || !("id" in value) || typeof value.id !== "string"
    || !("active" in value) || typeof value.active !== "boolean") return undefined;
  return { id: value.id, active: value.active };
}

// Acceptance checks
if(lookupAccount(()=>({id:"a",active:false}))?.active!==false||lookupAccount(()=>null)!==undefined||lookupAccount(()=>({id:"a",active:"false"}))!==undefined)throw new Error("contract");
Reasoning
The fresh projection exposes only checked fields and isolates external shape changes.

Interview reasoning and next step

In an interview, explain why .d.ts can be wrong without causing a compiler error inside the consuming project.

In an interview, explain why .d.ts can be wrong without causing a compiler error inside the consuming project. Describe how allowJs, checkJs and a boundary adapter support gradual adoption without a risky all-at-once rewrite.

The next lesson turns diagnostics into regression tests. Preserve both accepted and rejected uses so a dependency definition update cannot silently broaden an API or remove a useful validation boundary.

Optional stretch — after the core task works: Compare the adapter with a real package fixture.

Treat declaration files as external contracts, not as generated runtime implementations.