10 questions found
How does TypeScript's import/export syntax relate to the underlying JavaScript module system it compiles to?
Beginner
TypeScript's import/export statements are (mostly) standard ES Module syntax with type-only extensions layered on top; the 'module' compiler option in tsconfig.json controls what actual JavaScript module format (CommonJS, ESNext, ES2020, etc.) this syntax gets compiled down to for the target runtime.
// source.ts (ES Module syntax)
export function add(a: number, b: number): number { return a + b; }
// compiled output, if 'module': 'commonjs'
// exports.add = function (a, b) { return a + b; };
Real-world example
Choosing 'commonjs' as the module target for a Node.js backend versus 'esnext' for a modern bundler-based frontend.
Common follow-ups: How do you decide which 'module' setting to use for a given project type?
tsconfig & Compiler Options
How do you import just the types from a module without importing any runtime code, using 'import type'?
Beginner
'import type' explicitly imports only type information, which the compiler completely erases from the output JavaScript — useful for making it clear an import has zero runtime cost, and required in some strict module setups (isolatedModules) to disambiguate type-only imports.
import type { User } from './types';
import { fetchUser } from './api';
function display(user: User) { /* ... */ } // 'User' never exists in compiled JS output
Real-world example
Clearly signaling in a large codebase which imports are purely for type checking versus which pull in actual runtime dependencies.
Common follow-ups: What happens if you accidentally use 'import type' for something you also need as a runtime value?
Declaration Files
What does the 'esModuleInterop' compiler option fix when importing CommonJS modules?
Intermediate
Without esModuleInterop, importing a CommonJS module that exports a single function/value (via module.exports = x) using ES-style `import x from 'module'` syntax doesn't work correctly, because CommonJS and ES Modules have subtly incompatible default-export semantics; esModuleInterop adds compatibility shims so default imports 'just work' against CommonJS modules.
// Without esModuleInterop, this often fails or needs `import * as x`
// With esModuleInterop: true
import express from 'express'; // works cleanly, even though express uses module.exports
Real-world example
Enabling clean default-import syntax for Node.js libraries like express, lodash, or moment that use CommonJS's module.exports.
Common follow-ups: What alternative, more verbose import syntax would you need without esModuleInterop enabled?
tsconfig & Compiler Options
How does path mapping via the 'paths' compiler option let you use cleaner, absolute-style import paths?
Intermediate
The 'paths' option (used together with 'baseUrl') lets you define custom import aliases (like '@/components/*') that TypeScript resolves during type-checking — though note the bundler or runtime (Webpack, Vite, ts-node, Node itself) must be separately configured to understand the same aliases at build/run time, since TypeScript's path mapping is a compile-time-only convenience.
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "@components/*": ["src/components/*"] }
}
}
// usage.ts
import Button from '@components/Button'; // instead of '../../../components/Button'
Real-world example
Avoiding long, fragile relative import chains like '../../../../components/Button' in a deeply nested project structure.
Common follow-ups: Why doesn't configuring 'paths' alone make the aliases work at runtime in a plain Node.js script?
tsconfig & Compiler Options
What is 'isolatedModules' and why do some tools (like Babel, esbuild, or swc) require it to be enabled?
Intermediate
isolatedModules ensures every file can be safely transpiled INDEPENDENTLY, one file at a time, without needing full cross-file type information — some fast, non-typechecking compilers (Babel, esbuild) work this way for speed, and certain TypeScript patterns (like re-exporting only a type without 'export type') break that single-file assumption, so this flag catches them at typecheck time.
// With isolatedModules: true, this needs 'export type' explicitly:
export type { User } from './types'; // OK
// export { User } from './types'; // Error if User is type-only and isolatedModules is on
Real-world example
Ensuring a codebase remains compatible with a fast Babel or esbuild-based build pipeline that doesn't do full program-wide type analysis.
Common follow-ups: Does isolatedModules perform any type-checking itself, or is it purely a structural/syntax constraint?
tsconfig & Compiler Options
How do circular module dependencies in TypeScript behave differently depending on whether the compiled output targets CommonJS or ES Modules?
Advanced
The underlying circular-dependency behavior is inherited directly from the target JavaScript module system's own semantics (partially-initialized exports at the moment of the cycle) — TypeScript's type system itself doesn't change this runtime behavior, though its static analysis can sometimes make circularly-typed interfaces work at the type level even when runtime values would be problematic.
// a.ts
import { b } from './b';
export const a = 'A';
console.log(b); // may be undefined at this point due to the cycle, regardless of TS
// b.ts
import { a } from './a';
export const b = 'B';
Real-world example
Debugging a circular-import-caused undefined value bug that TypeScript's type checker didn't (and structurally couldn't) catch.
Common follow-ups: Why can't TypeScript's static type checker fully prevent this class of runtime circular-dependency bug?
ES Modules
How do ambient module declarations for non-code assets (like importing a .svg or .css file) work in a TypeScript + bundler setup?
Advanced
Bundlers like Webpack or Vite let you import non-JS assets directly, which isn't valid in plain JS/TS module resolution — you provide a wildcard ambient module declaration describing what type that import resolves to (often a string for the asset URL, or a React component for SVGs configured that way), letting TypeScript accept these bundler-specific imports.
// assets.d.ts
declare module '*.svg' {
const content: string;
export default content;
}
declare module '*.css' {
const styles: Record<string, string>;
export default styles;
}
// usage.tsx
import logo from './logo.svg'; // typed as string
Real-world example
Making a Webpack/Vite frontend project's non-JS asset imports (SVGs, CSS Modules, images) type-check correctly.
Common follow-ups: How would you type a CSS Modules import to get autocomplete on the specific class names it exports?
Declaration Files
How does 'module resolution' (the 'moduleResolution' compiler option) affect how TypeScript locates the right .d.ts file for a bare package import?
Advanced
moduleResolution controls the ALGORITHM TypeScript uses to search for a module's files/types given an import specifier (e.g. 'classic' vs 'node' vs 'bundler' strategies) — different settings walk node_modules, package.json 'exports' maps, or file-extension resolution differently, and mismatches here are a common source of 'Cannot find module' errors despite the package clearly being installed.
// tsconfig.json
{
"compilerOptions": {
"moduleResolution": "bundler" // matches modern bundler resolution behavior closely
}
}
Real-world example
Fixing a 'Cannot find module' error that only appears in TypeScript despite the import working fine at runtime via the bundler.
Common follow-ups: Why was the 'bundler' moduleResolution setting introduced as an alternative to 'node' / 'node16'?
tsconfig & Compiler Options
How do 'export *' and re-export barrel files (index.ts aggregating a directory's exports) interact with tree-shaking and TypeScript's type checking performance?
Advanced
Barrel files that re-export everything from many submodules make imports more convenient, but can hurt both bundler tree-shaking (some bundlers struggle to eliminate unused re-exported code through a barrel) and TypeScript compiler performance (the compiler must resolve and type-check the entire re-export graph even if a consumer only imports one specific named export).
// index.ts (barrel file)
export * from './Button';
export * from './Modal';
export * from './Tooltip';
// A consumer importing only { Button } may still pull in extra work/analysis for Modal and Tooltip
Real-world example
Debugging why a bundle unexpectedly includes an entire component library when only one component was actually imported.
Common follow-ups: What alternative import strategies help mitigate these barrel-file downsides in a large component library?
ES Modules
How would you correctly type a module that supports BOTH a default export and several named exports, and how do consumers import it under different esModuleInterop settings?
Advanced
Declare both `export default` and named `export`s normally in the module; how consumers import it depends on esModuleInterop and the module target — with esModuleInterop enabled, `import Default, { named } from 'module'` works cleanly, while under strict ES module interop rules without it, you may need `import * as mod from 'module'` and access `mod.default` explicitly.
// mathLib.ts
export default function add(a: number, b: number) { return a + b; }
export const PI = 3.14159;
// consumer.ts (with esModuleInterop: true)
import add, { PI } from './mathLib';
Real-world example
Designing a utility library's public API to offer both a convenient default export and additional named utility exports.
Common follow-ups: What subtle bug can occur if a CommonJS-compiled module's default export handling doesn't match what a consumer's bundler expects?
ES Modules