Learn why TypeScript throws TS2614 when a named import targets a module that only has a default export, and how to fix it in code and in libraries.
TypeScript error TS2614 means your braced import asked a module for a name it does not export — and the compiler spotted that the module does have a default export, so it appended a suggestion to use the default import form instead.
The plain version of this check is TS2305, "Module 'X' has no exported member 'Y'". TS2614 is the same failure with extra context. After the member lookup comes back empty, the checker looks at the module's default export; if one exists, it re-reports the error with the hint. That is why the two codes are easy to confuse in search results: same cause, different amount of help. The hint arrived in TypeScript 3.7 — on 3.6 and earlier the identical code reports TS2305 with no suggestion at all.
// The general shape of the error:
// Module '"./router"' has no exported member 'Router'. Did you mean to use 'import Router from "./router"' instead?
// ~~~~~~~~~ ~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// the specifier the name you asked for the module's default exportOne detail matters more than the wording: the suggestion fires because a default export exists, not because the default is what you wanted. Follow it blindly and you can end up with a binding that compiles but holds the wrong value.
The module ends in export default, and the import site wraps the name in braces. This is the textbook case, and it is especially common when a class or a React component is exported as the module's default.
// router.ts
export default class Router {
go(path: string) {
return path
}
}
// ❌ Broken — main.ts
import { Router } from "./router"
// ~~~~~~ Error: Module '"./router"' has no exported member 'Router'. Did you mean to use 'import Router from "./router"' instead?
export const appRouter = new Router()// ✅ Fixed — drop the braces, the default export binds to any name you choose
import Router from "./router"
export const appRouter = new Router()If the module is yours, there is a better fix one level up. Turning export default class Router into export class Router keeps the braced import working, makes the name canonical across the codebase, and lets editors rename the symbol everywhere at once. Default exports can be re-bound to any identifier at each import site, which is exactly what makes them awkward to refactor later.
// ✅ Better — router.ts exports the name instead
export class Router {
go(path: string) {
return path
}
}A *.vue file is compiled to a module with exactly one default export, and your project describes that shape with an ambient declaration. Import the component by name and the lookup fails against the shim.
// shims-vue.d.ts
declare module "*.vue" {
const component: { props: Record<string, unknown> }
export default component
}
// ❌ Broken — main.ts
import { UserCard } from "./UserCard.vue"
// ~~~~~~~~ Error: Module '"*.vue"' has no exported member 'UserCard'. Did you mean to use 'import UserCard from "*.vue"' instead?
export const card = UserCard// ✅ Fixed — a single-file component is always the default export
import UserCard from "./UserCard.vue"
export const card = UserCardNote the specifier in the message: it reads "*.vue", not "./UserCard.vue". The compiler prints the name of the ambient module that matched, so the hint looks nonsensical the first time you see it. It is not a bug — it is the wildcard declaration telling you which rule it applied.
A module builds an object, exports it as the default, and you import one of its properties with braces. The property is not an export, so the lookup fails — and the hint is actively misleading here, because it points at the object, not the property.
// analytics.ts
const analytics = {
track(event: string) {
return event
},
identify(userId: string) {
return userId
},
}
export default analytics
// ❌ Broken — main.ts
import { track } from "./analytics"
// ~~~~~ Error: Module '"./analytics"' has no exported member 'track'. Did you mean to use 'import track from "./analytics"' instead?
export const logged = track("order_placed")// ✅ Fixed — import the default, then reach for the member
import analytics from "./analytics"
export const logged = analytics.track("order_placed")Taking the suggestion literally would give you track bound to the whole analytics object, and the call would fail later with a different error. If several call sites only ever need one or two members, promote them to real named exports (export function track(...)) and let the default go.
Packages do move from named exports to a single default export across major versions, and the error then points at an import line you never touched. The lock file changed; your code did not.
// node_modules/http-client/index.d.ts — v3 of the package
export interface RequestOptions {
retry: number
}
declare const httpClient: {
get(url: string, options?: RequestOptions): Promise<string>
}
export default httpClient
// ❌ Broken — main.ts, written against v2
import { httpClient } from "http-client"
// ~~~~~~~~~~ Error: Module '"http-client"' has no exported member 'httpClient'. Did you mean to use 'import httpClient from "http-client"' instead?
export const body = httpClient.get("/api/orders")// ✅ Fixed — default for the value, braces for the types it still exports
import httpClient, { RequestOptions } from "http-client"
const options: RequestOptions = { retry: 2 }
export const body = httpClient.get("/api/orders", options)The two import forms combine in one statement, which is the shape most modern packages expect: the runtime entry point as the default, the types alongside it as named exports. When the error comes from inside node_modules rather than from your own file, the cause is usually a version mismatch between a package and its @types companion — align those versions first, and use skipLibCheck only to unblock a release while you do.
Open the module and read its last line. The answer is almost always visible there. export default X means the import takes no braces; export const X means it does. Your editor's go-to-definition on the failing name jumps straight to the module, which is faster than reasoning about the message.
Prefer fixing the export when you own the module. Replacing export default class Router with export class Router fixes every import site at once and keeps the symbol renameable. Reserve default exports for places where a framework requires them — Vue SFCs, Next.js pages, and lazy-loaded chunks.
Use the default import when you do not own the module. For third-party packages and generated declaration files, change your side: import httpClient from "http-client", and keep braces for the types the package still exports in the same statement.
Check whether you actually wanted a property, not an export. If the module's default is an object, the suggestion is wrong for you. Import the default and access the member (analytics.track(...)), or add a real named export for the member if several files need it.
Do not silence it with as any or a wildcard declare module. Both make the import resolve to any and hand you a runtime undefined instead of a compile error — the opposite of what you want from a mismatch this mechanical. If a dependency's own declarations are broken, pin the version, open an upstream issue, and set skipLibCheck: true as a temporary measure rather than editing your import into something untrue.
Settle on one export style per directory. Most TS2614 reports come from codebases where half the modules use default exports and half use named ones, so nobody can tell which form an import needs without opening the file. Picking named exports as the default rule — with documented exceptions for framework files — removes the guesswork for good.
A braced import asked a module for a name that is not in its export list, and the module has a default export. TypeScript reports the missing member and adds the "Did you mean to use import Y from X" suggestion on top. Without a default export present, the exact same mistake is reported as TS2305 instead, which is why both codes describe the same situation in slightly different words. The four scenarios behind nearly every occurrence are a default-exported class or component imported with braces, a *.vue shim, a property of a default-exported object, and a library that switched to a default export in a major release.
It depends on who owns the module. For your own code, change the export: export class Router instead of export default class Router makes the name part of the module's contract and keeps renames working across the project. For third-party packages, framework-mandated files like Vue single-file components, and generated declarations, change the import to the default form:
// your own module → fix the export
export class Router {}
// someone else's module → fix the import
import Router from "third-party-router"A namespace import (import * as router from "./router") also compiles, but it pulls in every member and defeats tree shaking, so keep it for the rare case where you really want the module object.
The suggestion is attached to the module, not to the name. Once the member lookup fails, the compiler only checks whether a default export exists; if it does, you get the hint, regardless of whether the default is the thing you were after. The default-exported object case shows this clearly: import { track } from "./analytics" suggests import track from "./analytics", which would bind the entire analytics object to a variable called track. Treat the hint as a statement about the module's shape — "this module has a default" — and then decide for yourself whether you want the default, a property of it, or a named export that should exist but does not.
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