Typorithm · hard

10. NodeNext, bundlers and package exports

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

10. NodeNext, bundlers and package exports

0%

Concept and contract

Align compiler lookup with the environment that actually loads emitted code.

An import can type-check and still fail at runtime when compiler lookup and runtime loading disagree. Choose module resolution for the actual host: a bundler, Node ESM, Node CommonJS or a library consumed by several environments. No single configuration is universally correct.

Keep package.json type, exports, compiler settings and emitted entrypoints together in a minimal fixture. This makes extension rules and import conditions observable. Do not copy an application’s bundler configuration into a published library without testing its consumers.

Mechanism and worked example

The example’s resolver reads a finite registry through a safe key contract.

The example’s resolver reads a finite registry through a safe key contract. In a real package, the registry can be exported from an explicit public entrypoint while internal modules stay private. Package exports control supported import paths; they do not automatically make internal source names stable APIs.

For NodeNext ESM, write relative import specifiers compatible with emitted JavaScript, commonly using .js in TypeScript source. Bundler resolution can accept patterns the direct Node runtime cannot. A paths alias does not generally rewrite emitted specifiers into a working runtime location.

Complete worked example

typescript

export const routes={roles:"/vacancies",news:"/news"} as const;
export type Route=keyof typeof routes;
export function path(route:Route):string{return routes[route];}
const selected=path("roles");
Reasoning
A finite registry illustrates a deliberate public vocabulary; emitted imports must still match their runtime host.

Counterexamples and common mistakes

The rejected registry key illustrates a finite public surface.

The rejected registry key illustrates a finite public surface. A package-level equivalent is an unsupported deep import: consumers should use documented exports instead of reaching into private files. A compiler workaround cannot make an absent emitted file appear.

Do not use baseUrl as a permanent migration escape hatch for TypeScript 7. It is removed there. Review explicit paths, package imports and runtime support, then verify emitted resolution rather than merely suppressing the configuration diagnostic.

Rejected counterexample: expected compiler diagnostic

typescript

const routes={roles:"/vacancies",news:"/news"} as const;
// @ts-expect-error This is not a supported public route.
const invalid: keyof typeof routes="private";
Reasoning
Unsupported public names should not be accepted merely to quiet a resolution error.

Independent practice

Create two files: routes.ts exporting a type and a runtime registry, and main.ts importing both with explicit type-only syntax for the type.

Create two files: routes.ts exporting a type and a runtime registry, and main.ts importing both with explicit type-only syntax for the type. Test a bundler-oriented build and a separate NodeNext ESM build using appropriate package metadata.

Begin with one relative import and no aliases. Only add an alias after the executable baseline works. Optional extension: expose the registry through a package exports map and verify a consumer cannot rely on an undocumented private path.

Step 1: Establish one working relative import. Step 2: Match emitted imports to the declared execution host.

Practice starter: solve before reading the solution

typescript

export const routes={roles:"/vacancies",news:"/news"} as const;
export type Route=keyof typeof routes;
// Put the consumer in a second file and import Route as a type.
Reasoning
Establish a working two-file relative import before introducing aliases or export conditions.
  • Hint 1: Import erased contracts with import type.
  • Hint 2: An editor alias is not automatically a runtime alias.

Solution, tests and trade-offs

The solution keeps the runtime registry and its derived Route type in one module.

The solution keeps the runtime registry and its derived Route type in one module. The consumer should import Route with import type and the registry as a value. For direct Node ESM, the relative source import names the emitted .js module.

Compile and execute the emitted consumer under its declared host. Inspect package exports and output files when a resolution error occurs. This is a deployment contract test, not just a question of making an editor’s import autocomplete succeed.

Practice solution and acceptance checks

typescript

export const routes={roles:"/vacancies",news:"/news"} as const;
export type Route=keyof typeof routes;
export function path(route:Route):string{return routes[route];}

// Acceptance checks
if(path("news")!=="/news")throw new Error("public surface");
Reasoning
The exported type is erased while the registry and resolver remain runtime values.

Interview reasoning and next step

In interviews, distinguish module syntax, resolution and runtime execution.

In interviews, distinguish module syntax, resolution and runtime execution. Explain why an alias can work in an editor but fail after emission. Discuss public package entrypoints and conditions with a concrete consumer rather than listing every compiler flag.

Next, generate declarations and test the public package surface. Correct lookup is only one half of a reusable library; consumers also need accurate types that do not leak private implementation dependencies.

Optional stretch — after the core task works: Verify a public exports map from an external consumer.

Align compiler lookup with the environment that actually loads emitted code.