Learn why TypeScript throws TS1192 when a default import targets a module that only has named exports, and how to fix it on both sides of the import.
TS1192 means you wrote a default import — import something from "./module" — and the module on the other side does not have a default export. A default import is not a wildcard: it binds to one specific export whose name is literally default. If the module only ships named exports, there is nothing for that binding to point at, and the compiler stops you before the undefined shows up at runtime.
TypeScript raises this while resolving the import, which is why it fires even in files that otherwise type-check perfectly. The check runs against the module's real export list, so it is accurate in both directions: a module that has a default satisfies it, a module that only has export const / export function does not.
// The general shape of the error:
import config from "./config"
// ~~~~~~ Error: Module '"./config"' has no default export.The interop flags are the part people get wrong. esModuleInterop and allowSyntheticDefaultImports do create a synthetic default — but only for modules TypeScript believes are CommonJS-shaped, such as a declaration file that uses export =. For a real ES module, the compiler knows the default genuinely is not there, and no flag will make it appear. That is working as intended, not a bug.
The everyday case. A small configuration or helper module exports a handful of constants, and the import site reaches for the default out of habit.
// ❌ Broken — config.ts
export const apiUrl = "/api"
export const timeout = 3000
// main.ts
import config from "./config"
// ~~~~~~ Error: Module '"./config"' has no default export.
export const url = config.apiUrl// ✅ Fixed — import the names the module actually exports
import { apiUrl, timeout } from "./config"
export const url = `${apiUrl}?t=${timeout}`If you really do want the whole module as one object, a namespace import gives you that without touching the exporting file:
// ✅ Also fixed — a namespace import collects every named export
import * as config from "./config"
export const url = `${config.apiUrl}?t=${config.timeout}`Adding export default { apiUrl, timeout } to config.ts would also silence the error, but reach for it only if a default is genuinely the right shape for that module. Named exports survive renaming, tree-shaking and auto-import far better.
export *export * re-exports every named export of a module and deliberately skips the default. So a barrel that looks like it forwards a component in fact forwards everything except the one binding you want.
// ❌ Broken — button.ts
export default function Button(label: string) {
return label
}
export const buttonVariants = ["primary", "ghost"] as const
// index.ts
export * from "./button"
// main.ts
import Button from "./index"
// ~~~~~~ Error: Module '"./index"' has no default export.// ✅ Fixed — index.ts gives the default a name on the way through
export { default as Button } from "./button"
export { buttonVariants } from "./button"
// main.ts
import { Button } from "./index"This is the one cause where the error message actively misleads: it names the barrel, not the file that owns the component, so the search usually starts in the wrong place. If import Button from "./button" works and import Button from "./index" does not, you have this.
Note that export { default as Button } from "./icons" against a module with no default reports TS2305 (has no exported member 'default') rather than TS1192 — same underlying mistake, different diagnostic, because the compiler sees it as a missing named member of a re-export.
esModuleInteropWith "module": "commonjs" and no interop flag, import fs from "fs" is checked literally: does the fs module declaration have a default export? It does not.
// ❌ Broken — tsconfig with "module": "commonjs" and esModuleInterop off
import fs from "fs"
// ~~ Error: Module '"fs"' has no default export.
export const raw = fs.readFileSync("orders.json", "utf8")// ✅ Fixed — a namespace import needs no interop flag at all
import * as fs from "fs"
export const raw = fs.readFileSync("orders.json", "utf8")The better project-wide fix is to turn the flag on:
{
"compilerOptions": {
"esModuleInterop": true
}
}With esModuleInterop: true, the original import fs from "fs" compiles and emits a runtime helper that makes the default work. Prefer it over allowSyntheticDefaultImports on its own: that flag only quiets the type checker, so in a project that emits with tsc the types pass while require("fs").default is undefined at runtime. Enable esModuleInterop (which implies allowSyntheticDefaultImports) and the two stay in step.
Not every CommonJS package lands here. A library typed with export = someValue, such as lodash or moment, reports TS1259 (can only be default-imported using the 'esModuleInterop' flag) instead. Same remedy, different code — fs gets TS1192 because its typings use plain named exports.
module: nodenextHere the interop flags are already on — nodenext implies them — and the error appears anyway. That surprises people, but it is the correct answer: the package declares itself as ESM, so TypeScript knows exactly what it exports and will not fabricate a default.
// ❌ Broken — dependency with "type": "module" in its package.json,
// compiled with "module": "nodenext" / "moduleResolution": "nodenext"
import pricing from "@acme/pricing"
// ~~~~~~~ Error: Module '"@acme/pricing"' has no default export.
export const label = pricing.formatPrice(1999)// ✅ Fixed — named imports, which is all an ESM package offers here
import { formatPrice } from "@acme/pricing"
export const label = formatPrice(1999)The same rule applies to your own source files: a local .ts module with only export const keeps reporting TS1192 under a default import no matter how the interop flags are set, because the compiler can see the file and knows there is no default. Flags only help where the module shape is ambiguous.
One last pairing worth knowing: the mirror image of this error, a named import from a module that only has a default, is TS2614 — Module '"./logger"' has no exported member 'createLogger'. Did you mean to use 'import createLogger from "./logger"' instead?. If you are flipping an import back and forth between the two forms, you are bouncing between TS1192 and TS2614, and the module's export list settles which one is right.
Open the module and read its exports. The error names the resolved module path; go there first. export default present? Then the import is fine and the path is resolving somewhere unexpected — check for a same-named file in another folder, a path alias, or a stale .d.ts. No export default? Then the import side has to change, which is steps 2 and 3.
Switch to the import form that matches. Named exports want import { apiUrl, timeout } from "./config"; everything at once wants import * as config from "./config". Both are checked against the real export list, so a typo in a name becomes a compile error instead of an undefined at runtime. Your editor's auto-import writes the correct form if you let it complete the identifier rather than typing the import line by hand.
Add export default only when the module genuinely has one main thing. A React component file, a configured client instance, a single class — those are reasonable defaults. A grab-bag of helpers is not; bolting export default { ... } onto it to silence the error makes the module harder to tree-shake and harder to auto-import.
Fix barrels by naming the default on the way through. export { default as Button } from "./button" next to the export * line. Do this once per barrel, when you create it, and the "it works from the file but not from the index" puzzle never comes up.
Set esModuleInterop: true for CommonJS dependencies — not allowSyntheticDefaultImports alone. The interop flag fixes the types and emits the runtime helper; the synthetic-defaults flag only fixes the types and leaves require(...).default undefined. If a bundler such as Vite or webpack accepts a default import that tsc rejects, align the tsconfig with the bundler rather than ignoring one of them.
Do not paper over it with as any or // @ts-ignore. TS1192 is one of the few errors that reliably predicts a runtime failure: the binding you imported will be undefined, and the crash lands on the first property access, far from the import. Suppressing it converts a compile error into a production one.
A default import binds to a single export whose name is default. TS1192 means TypeScript resolved the module, looked for that export, and did not find one — most often because the module only has named exports.
The two common variants are a module that was never given a default (export const, export function only) and a barrel file whose export * quietly skipped the default it was supposed to forward. In both cases, the compiler is reading the module's genuine export list, so the fix is to match it: import the names that exist, or add the default to the module.
Because those flags synthesize a default only where the module shape is ambiguous — typically a declaration file using export =, which describes a CommonJS value that older bundlers would have exposed as a default. That is interop, and it is allowed to guess.
When the target is a real ES module — a .ts file in your project, or a dependency that declares "type": "module" — there is no guessing to do. TypeScript can see the export list, knows default is not on it, and reports TS1192 regardless of esModuleInterop or allowSyntheticDefaultImports. This is deliberate compiler behaviour, not a missing feature.
// Still an error with esModuleInterop: true
import config from "./config" // config.ts exports only named bindingsThe fix is on the import side (use named imports) or on the module side (add a real export default). No compiler option is going to invent the binding for you.
Pick the names you need: import { formatPrice, parsePrice } from "@acme/pricing". This is the form to prefer — it documents the dependency precisely and lets bundlers drop the rest of the module.
If you want the module as a single object, use a namespace import: import * as pricing from "@acme/pricing", then call pricing.formatPrice(1999). It behaves like the default import you were reaching for, without requiring the module to have a default.
What you should not do is add export default { ... } to the module just so the original line compiles. It works, but it gives every consumer a second way to import the same module, and the two forms drift apart as the module grows.
Browse all TypeScript practice challenges to keep sharpening your type-level skills.
Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges