Typorithm · hard

11. Public declarations and library compatibility

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

11. Public declarations and library compatibility

0%

Concept and contract

Review the types consumers receive, not only the source that produced them.

A library’s declaration output is part of its public API. Consumers may use a different compiler version and may not have the source repository’s aliases, private packages or build plugins. Test a small external consumer against the actual published entrypoints and declarations.

Explicit exported types can make contracts easier to review and avoid leaking inferred implementation details. Do not annotate every local variable mechanically; focus on boundaries where an accidental inferred change would alter the public surface or reference an unavailable internal type.

Mechanism and worked example

The example exports a readonly RoleView contract and a projector returning it.

The example exports a readonly RoleView contract and a projector returning it. The implementation may change internally while the public fields remain stable. Declaration emission should expose the contract and function signature without importing private database or framework implementation types.

declaration and emitDeclarationOnly generate declarations, but they do not validate the runtime package layout. isolatedDeclarations can support faster declaration workflows under its requirements; it does not replace full semantic checking of the application or prove runtime compatibility.

Complete worked example

typescript

export type RoleView={readonly id:string;readonly title:string};
export function roleView(value:{id:string;title:string;secret:string}):RoleView{return {id:value.id,title:value.title};}
Reasoning
A named public contract avoids exposing the internal secret-bearing input as the output surface.

Counterexamples and common mistakes

The rejected consumer writes a readonly public field.

The rejected consumer writes a readonly public field. This protects checked consumers from an unsupported mutation API. Runtime freezing remains separate, and a JavaScript consumer can still mutate a plain object unless the implementation enforces a stronger ownership policy.

Do not publish declarations that point to local workspace paths unavailable to consumers. Likewise, a declaration using newer syntax can break older supported compilers. State the compiler support range and test it rather than assuming every consumer runs the author’s editor version.

Rejected counterexample: expected compiler diagnostic

typescript

type RoleView={readonly id:string;readonly title:string};
const role:RoleView={id:"a",title:"QA"};
// @ts-expect-error The public view does not expose mutation.
role.id="b";
Reasoning
Readonly describes supported static access, not automatic runtime freezing.

Independent practice

Build a public SearchOptions type with a required query and optional limit, then export normalizeOptions returning an explicit normalized contract with a required numeric limit.

Build a public SearchOptions type with a required query and optional limit, then export normalizeOptions returning an explicit normalized contract with a required numeric limit. Generate declarations and consume them from a separate minimal fixture.

Test empty query trimming and the default limit. Optional extension: add declaration snapshot review for the exported surface. Keep the first task small enough that a learner can read the complete declaration file and explain every exported name.

Step 1: Return an explicitly named normalized contract. Step 2: Supply the required output limit at runtime.

Practice starter: solve before reading the solution

typescript

export type SearchOptions={query:string;limit?:number};
export type Normalized={query:string;limit:number};
export function normalizeOptions(options:SearchOptions):Normalized{throw new Error("TODO");}
Reasoning
Inspect a complete small declaration from a consumer outside the repository configuration.
  • Hint 1: Inspect emitted declarations from a separate consumer.
  • Hint 2: Declaration generation does not test runtime package layout.

Solution, tests and trade-offs

The solution constructs the normalized object explicitly and returns a named type.

The solution constructs the normalized object explicitly and returns a named type. The public output has no optional limit, because the implementation supplies a default. Runtime validation of negative limits would be a separate policy and should not be silently implied by the number type.

Inspect the emitted .d.ts and compile a consumer without repository-only aliases. Execute the implementation tests as well. A declaration can accurately describe a wrong algorithm, so declaration compatibility and runtime behavior are complementary checks.

Practice solution and acceptance checks

typescript

export type SearchOptions={query:string;limit?:number};
export type Normalized={query:string;limit:number};
export function normalizeOptions(options:SearchOptions):Normalized{return {query:options.query.trim(),limit:options.limit??20};}

// Acceptance checks
if(normalizeOptions({query:" QA "}).query!=="QA"||normalizeOptions({query:""}).limit!==20||normalizeOptions({query:"x",limit:0}).limit!==0)throw new Error("normalize");
Reasoning
The required normalized limit is justified by an executable default, not a type assertion.

Interview reasoning and next step

In interviews, explain source API versus published declaration API and identify one way an inferred internal type can leak.

In interviews, explain source API versus published declaration API and identify one way an inferred internal type can leak. Describe how you would test a package from outside its own workspace. Mention compiler-version support as part of the library contract.

Next, combine type regressions, runtime assertions and interaction tests in one small verification plan. A production-quality TypeScript feature needs evidence at each boundary rather than a single green compiler command.

Optional stretch — after the core task works: Add a public declaration snapshot review.

Review the types consumers receive, not only the source that produced them.