Typorithm · medium

12. Type-only imports and runtime module resolution

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

12. Type-only imports and runtime module resolution

0%

Concept and contract

Choose module settings for the actual execution host instead of treating import syntax as sufficient.

A module can export runtime values, type declarations or both. Type-only imports make a dependency’s role explicit and are erased from emitted JavaScript. A normal import may remain because its runtime binding or side effects matter. Distinguishing these roles helps prevent accidental cycles.

Choose module and moduleResolution together for the host. Bundler mode describes a bundler that resolves source imports; NodeNext models Node’s package and extension rules. A configuration that type-checks for a bundler is not evidence that emitted files run directly under Node.

Mechanism and worked example

The worked file exports a public Role type and a runtime formatter.

The worked file exports a public Role type and a runtime formatter. A separate consumer imports Role with import type and the formatter as a value. With verbatimModuleSyntax, write the intended distinction explicitly rather than relying on the compiler to guess which imports should disappear.

In an ESM Node package, relative emitted imports commonly need .js extensions in source imports so the resulting JavaScript resolves correctly. A bundler may accept extensionless imports. Keep a package.json type setting, compiler configuration and executable smoke test alongside the chosen example.

Complete worked example

typescript

export type Role = { title: string };
export function formatRole(role: Role): string { return role.title.toUpperCase(); }
Reasoning
The alias is erased while formatRole remains an exported JavaScript function.

Counterexamples and common mistakes

The rejected type-only import is used as a runtime value.

The rejected type-only import is used as a runtime value. An interface has no JavaScript constructor to log or instantiate. Fixing the error requires understanding whether the consumer needs a type, a real value export, or both, rather than changing import spelling randomly.

paths aliases help the compiler resolve names but do not generally rewrite emitted JavaScript imports. Configure the runtime or bundler consistently, or use supported package imports and exports. A successful editor lookup cannot substitute for running the emitted entrypoint.

Rejected counterexample: expected compiler diagnostic

typescript

type Role = { title: string };
// @ts-expect-error A type alias has no runtime value.
const constructor = Role;
Reasoning
A type name is not a constructor or other runtime value.

Independent practice

Create a shared Result type and an unwrap runtime function in one module, then consume them from another.

Create a shared Result type and an unwrap runtime function in one module, then consume them from another. Use explicit type-only imports for Result and a value import for unwrap. Keep the type export independent of a framework-specific runtime dependency.

Compile the two-file exercise with an explicit configuration and execute its emitted entrypoint in the intended host. Check both success and failure paths. The displayed single-file starter is the module’s implementation; split the exports and imports as the task describes.

Step 1: Export Result and unwrap. Step 2: Import the type and runtime function separately.

Practice starter: solve before reading the solution

typescript

export type Result<T> = { ok: true; value: T } | { ok: false; reason: string };
export function unwrap<T>(result: Result<T>): T { throw new Error("TODO"); }
Reasoning
Separate shared contracts from values and test the emitted consumer in its intended host.
  • Hint 1: Use import type for the erased contract.
  • Hint 2: Execute the emitted consumer in the declared host.

Solution, tests and trade-offs

The solution exports a tagged generic Result and an unwrap implementation that throws the failure reason.

The solution exports a tagged generic Result and an unwrap implementation that throws the failure reason. In the consumer, import type { Result } and import { unwrap } preserve the distinction between erased contracts and required runtime code.

Inspect emitted imports and run the consumer, not just the implementation file. To compare NodeNext and Bundler, use separate configurations and package metadata. Do not label an unresolved import as a typing problem if the runtime simply cannot locate the emitted file.

Practice solution and acceptance checks

typescript

export type Result<T> = { ok: true; value: T } | { ok: false; reason: string };
export function unwrap<T>(result: Result<T>): T {
  if (!result.ok) throw new Error(result.reason);
  return result.value;
}

// Acceptance checks
if(unwrap({ok:true,value:3})!==3)throw new Error("unwrap");let failed=false;try{unwrap({ok:false,reason:"offline"})}catch{failed=true}if(!failed)throw new Error("failure");
Reasoning
unwrap preserves T on success and exposes its failure behavior through implementation and tests.

Interview reasoning and next step

In an interview, explain type position versus value position and why class declarations occupy both spaces.

In an interview, explain type position versus value position and why class declarations occupy both spaces. Describe one import that must stay for side effects and one that must disappear because it names only an interface.

The production course returns to package exports, declarations and compiler migration. First, finish foundational class and JavaScript interoperability contracts so the shared modules contain intentional, testable public surfaces.

Optional stretch — after the core task works: Repeat the fixture under NodeNext ESM.

Choose module settings for the actual execution host instead of treating import syntax as sufficient.