TypeScript / Intermediate / Lesson 10
Modules, resolution, and declarations
Align the checker with the actual module host, distinguish type-only dependencies from runtime imports, and treat declaration files as claims about real implementations.
On this page
Lesson goals
- Explain ESM, CommonJS, and host-dependent resolution.
- Use type-only imports and exports intentionally.
- Diagnose declaration and path-alias assumptions without inventing runtime support.
The runtime or bundler defines loading behavior
JavaScript module loading is performed by a host such as Node, a browser, or a bundler. TypeScript needs a compatible model to resolve imports and emit code, but it does not replace that host. ESM and CommonJS differ in syntax, file interpretation, package metadata, and interoperability. Choose module and moduleResolution together for the intended environment. NodeNext models modern Node rules rather than a universal portable import policy. The exact behavior can depend on file extensions and package.json type. Record those details with the build command instead of assuming every editor-resolved import will execute successfully.
Export runtime operations and type descriptions deliberately
The first complete module exports a type and a function, then exercises the function locally. export type describes a checking dependency and is erased, while the function remains a runtime value. Another module would need the correct emitted path and module format to import it. This local demonstration avoids pretending an unspecified package layout is runnable. In real code, import type prevents a type-only relationship from becoming an unnecessary runtime import under appropriate compiler options. It does not make a genuinely needed runtime constructor or side effect disappear safely.
export type Count = { value: number };
export function describe(count: Count): string { return `count=${count.value}`; }
console.log(describe({ value: 3 }));
count=3
Count supplies checking information. describe is emitted and returns the printed string.
Declarations describe values but cannot supply them
A .d.ts file describes an implementation that must exist elsewhere. Declaring a global or module can remove a missing-name diagnostic while leaving a runtime ReferenceError or loading failure untouched. The second complete example supplies an actual object matching its interface, demonstrating what a declaration alone cannot do. When consuming a library, verify the package exports, JavaScript entry points, declaration entry points, and supported module formats. A declaration must match real behavior, including optional values and errors. skipLibCheck can skip declaration checking, but it does not repair inaccurate declarations or make absent runtime code available.
export {};
interface Formatter { format(value: number): string }
const formatter: Formatter = { format: value => `value:${value}` };
console.log(formatter.format(8));
value:8
The method works because the object literal creates it. An ambient declaration of formatter would not create that object.
A path alias is not an emitted-path rewrite
paths can help the checker resolve an import according to a configured mapping, but it does not normally rewrite the emitted module specifier to a relative file. The runtime or bundler needs corresponding support, or the source must use resolvable specifiers. Under Node ESM, relative imports commonly name the emitted .js extension even when source is .ts, with the checker resolving the source according to the selected rules. Compiler features that rewrite specific TypeScript extensions have version and scope requirements; do not generalize them into arbitrary alias rewriting. Test the emitted entry point in its intended host.
Verify the package boundary as a consumer
Use a small isolated multi-file fixture with the same package metadata, compiler options, import extensions, and runtime command as the intended application. Check both tsc resolution and execution; success in only one stage is incomplete evidence. For published libraries, test a packed artifact from a separate consumer rather than source-tree paths that may bypass exports. Keep platform-dependent configuration fragments non-runnable until their surrounding files are provided. Pin the compiler version when diagnosing upgrades because resolution and declaration behavior can evolve. Avoid broad ambient wildcard declarations that silence every unresolved import while hiding which runtime dependencies are actually missing.
Common errors and fixes
A path alias checks but fails when Node loads it.
- Why it happens
- The host has no matching alias resolver.
- How to fix it
- Use a runtime-resolvable specifier or supported host configuration.
- Prevent it next time
- Run emitted entry points in verification.
A declared global is missing at runtime.
- Why it happens
- Ambient types were mistaken for implementation.
- How to fix it
- Provide the real dependency or remove the invalid assumption.
- Prevent it next time
- Verify declarations against package JavaScript.
ESM imports are emitted for an incompatible host.
- Why it happens
- Module options and package interpretation disagree.
- How to fix it
- Align module, resolution, extensions, and package metadata.
- Prevent it next time
- Record a complete multi-file fixture.
Check your understanding
1. An import named @app/math resolves through paths, but the emitted JavaScript still contains @app/math. Explain the two independent checks required.
Show worked solution
First verify that the checker maps the source import to the intended declaration or source file. Then verify that the runtime or bundler can load the same emitted specifier. paths success establishes only the first. A valid repair is a resolvable relative import or an explicitly supported alias mechanism in the host, tested by running the emitted program.
2. A third-party package ships a .d.ts declaring format(number): string but its JavaScript returns undefined. Which layer is wrong, and what evidence should a minimal report include?
Show worked solution
The declaration and implementation contract disagree; TypeScript cannot infer the actual runtime behavior merely from that declaration. Include package and compiler versions, import style, resolved declaration path, a tiny input, and observed JavaScript output. Correct or replace the inaccurate contract with a verified implementation rather than adding an assertion that claims the undefined result is a string.
Key takeaways
- Match module settings to the real host.
- Type-only dependencies and runtime values differ.
- Verify emitted paths and declarations through actual consumers.