Learn why TypeScript throws TS2742 when declaration emit can't name an inferred type, and how to fix it with annotations, dedupe or pnpm hoisting.
TS2742 means TypeScript worked out the type of something you exported, then discovered it has no portable way to write that type down in the .d.ts file it is generating. The type is fine. Naming it is the problem.
Declaration emit is the mechanism. When you compile with declaration: true, composite: true, emitDeclarationOnly or isolatedDeclarations, TypeScript produces a .d.ts alongside your JavaScript, and every export in it needs a written-out type. For export const dbClient = createClient() the compiler has to spell out what createClient() returns — say import("query-core").QueryBuilder. That only works if query-core is a module your package can name. When the only available specifier is something like db-client/node_modules/query-core — a copy nested inside another package, reachable on your disk and nowhere else — the emitted declaration would break for everyone who installs your library. So the compiler stops and asks you to annotate the export yourself.
export const dbClient = createClient()
~~~~~~~~
error TS2742: The inferred type of 'dbClient' cannot be named without a reference to
'db-client/node_modules/query-core'. This is likely not portable. A type annotation is necessary.Two things follow from this that surprise people. First, the error never appears under tsc --noEmit: the type checks perfectly well, and only the emitter objects. Second, nothing is wrong with your dependency tree in the ordinary sense — the package is installed and the types resolve. What is missing is a name, and the fix is almost always to supply one.
You will also meet this error under a second code. TypeScript 5.x introduced TS2883, which reports the same situation but additionally names the symbol it was trying to write ('QueryBuilder' from 'db-client/node_modules/query-core'), and TypeScript 6.0 emits it in most cases where earlier versions emitted TS2742. Everything on this page applies to both.
The canonical case. You depend on db-client; db-client returns a type that belongs to query-core, which you never declared. Under npm and Yarn that usually still works, because hoisting puts query-core at the top level where it is importable by name. The moment a version conflict forces a nested copy — or pnpm's isolated layout keeps it out of your node_modules on principle — the name disappears.
// ❌ Broken — query-core is only present at db-client/node_modules/query-core
import { createClient } from 'db-client'
export const dbClient = createClient()
// ~~~~~~~~
// Error: The inferred type of 'dbClient' cannot be named without a reference to
// 'db-client/node_modules/query-core'. This is likely not portable. A type annotation
// is necessary. (TS2742)// ✅ Fixed — ReturnType names the type through a module you *can* import
import { createClient } from 'db-client'
export const dbClient: ReturnType<typeof createClient> = createClient()The annotation works because typeof createClient is written in terms of db-client, which is a direct dependency and therefore nameable from the generated declaration. The emitted file reads export declare const dbClient: ReturnType<typeof createClient> and resolves for every consumer.
The other fix is to make the module nameable. If query-core is at the top level rather than nested, TypeScript writes import("query-core").QueryBuilder on its own and the error disappears — which is what npm dedupe, aligning conflicting version ranges, or adding the package to your own dependencies achieve. On pnpm the equivalent knobs are declaring the dependency directly or a public-hoist-pattern[] entry in .npmrc; shamefully-hoist=true also works but gives up the strictness that made you choose pnpm.
constEvery position that declaration emit has to write out can trigger this, so the same dependency tree produces the error in several places at once. Function return types and class property types are the two you will see most, and the message names the function or the property rather than a variable.
// ❌ Broken — both the return type and the property type are inferred
import { createClient } from 'db-client'
export function openConnection() {
// ~~~~~~~~~~~~~~
// Error: The inferred type of 'openConnection' cannot be named without a reference
// to 'db-client/node_modules/query-core'. (TS2742)
return createClient()
}
export class OrderRepository {
readonly query = createClient()
// ~~~~~
// Error: The inferred type of 'query' cannot be named without a reference to
// 'db-client/node_modules/query-core'. (TS2742)
}// ✅ Fixed — one named alias, annotated everywhere it is needed
import { createClient } from 'db-client'
type Query = ReturnType<typeof createClient>
export function openConnection(): Query {
return createClient()
}
export class OrderRepository {
readonly query: Query = createClient()
}A local type alias is worth the extra line here. It gives you one place to change if the dependency moves, and it keeps the annotations short enough that people will actually write them. Note that only exported declarations are affected — a const used privately inside the module never reaches the .d.ts, so it never needs a name.
export default Of An Inferred ValueDefault exports produce a confusing version of the message: there is no identifier to name, so the compiler calls it 'default'. This is the shape behind the widely reported failures in eslint.config.mts files that do export default tseslint.config(...), where the config helper's return type comes from a package the flat-config file never declares.
// ❌ Broken — the export has no name, so the message says 'default'
import { createClient } from 'db-client'
export default createClient()
// ~~~~~~~~~~~~~~~~~~~~~~~~~~
// Error: The inferred type of 'default' cannot be named without a reference to
// 'db-client/node_modules/query-core'. This is likely not portable. A type annotation
// is necessary. (TS2742)// ✅ Fixed — name the value first, annotate it, then export it
import { createClient } from 'db-client'
const dbClient: ReturnType<typeof createClient> = createClient()
export default dbClientYou cannot annotate export default <expression> in place, so the two-step form is the fix rather than a style preference. For a config file specifically, the more direct annotation is usually the library's own published type — import type { Linter } from 'eslint' and then const config: Linter.Config[] = tseslint.config(...) — because that documents the file's contract as well as silencing the error.
composite: true Turning On Declaration Emit You Never Asked ForThe most disorienting version of this error hits projects that do not publish types at all. composite: true implies declaration: true, so adding a file to a project-references build switches on the emitter for it. Nothing about your code changed; the compiler simply started generating .d.ts files for it.
// tsconfig.json — composite implies declaration, so .d.ts files get emitted
{
"compilerOptions": {
"strict": true,
"composite": true,
"outDir": "out"
},
"include": ["src/**/*"]
}// ❌ Broken — the same export that was fine under noEmit now fails
import { createClient } from 'db-client'
export const dbClient = createClient()
// ~~~~~~~~
// Error: The inferred type of 'dbClient' cannot be named without a reference to
// 'db-client/node_modules/query-core'. (TS2742)// ✅ Fixed — annotate, exactly as in a published package
import { createClient } from 'db-client'
export const dbClient: ReturnType<typeof createClient> = createClient()If the file is an application entry point or a tool config rather than something another project consumes, the alternative is to keep it out of the composite project — move it to an exclude entry or a sibling tsconfig without composite. That is a legitimate fix, not a workaround, as long as nothing actually references the project's declarations. What you should not do is set declaration: false on a package that publishes types, which trades a build error for consumers silently getting any.
Annotate the export the message names. This is the fix that always works and never depends on the shape of anyone's node_modules. Read the identifier in quotes, find its declaration, and give it a type. When the obvious type is the one you cannot name, ReturnType<typeof someFactory> or InstanceType<typeof SomeClass> will usually express it through a module you can name. Many libraries also re-export the type directly — check for import type { Client } from 'the-package' before reaching for a utility type.
Make the module nameable by installing it directly. If the message points at something/node_modules/pkg, adding pkg to your own dependencies (or devDependencies for a @types/* package) at a compatible version gives TypeScript a portable specifier, and the error disappears for every export at once. This is the right fix when the type genuinely is part of your public API, because consumers need that package resolvable anyway.
Dedupe the tree when the nesting is accidental. A nested copy usually means two packages want incompatible versions of the same dependency. npm ls query-core shows you who, npm dedupe collapses the copies where the ranges allow it, and an overrides block in package.json (resolutions for Yarn) forces the issue when they do not. On pnpm, declare the dependency or add a public-hoist-pattern[] line to .npmrc; reach for shamefully-hoist=true only as a last resort, since it undoes the isolation pnpm exists to provide.
Check whether you need declaration emit at all. Run tsc --noEmit — if the error vanishes, it is purely an emit concern. Applications that never publish a .d.ts can turn declaration off, and a tool config file that landed in a composite project can simply be excluded from it. Libraries do not get this option: their consumers read the declarations.
Do not silence it with @ts-ignore or as any. A suppression comment does not stop the emitter from writing a broken type, and as any writes any into your published API, so every consumer loses type safety at that export and the mistake is invisible until someone reports a bug. Annotating is barely more work and produces a .d.ts you would be happy to read.
Keep the public surface explicitly typed. The durable prevention is to annotate exported values, function return types and class members in any package that emits declarations, rather than relying on inference at the boundary. TypeScript 5.5's isolatedDeclarations flag enforces exactly this and turns every such spot into an up-front error instead of a surprise during a release build — worth enabling in a library, and it makes declaration emit dramatically faster as a bonus.
TS2742 fires during declaration emit when TypeScript cannot write an exported value's inferred type into the generated .d.ts. To emit export declare const dbClient: … it needs a module specifier for the type's home module, and if the only one available is a path like db-client/node_modules/query-core — a copy nested inside another package — that specifier would not resolve for anyone who installs your package. Rather than emit something broken, the compiler asks you to annotate the export so you decide what the declaration says. The giveaway is that tsc --noEmit reports nothing: this is an emit error, not a type error, which is why it appears in library builds, composite projects and emitDeclarationOnly runs and nowhere else.
It does not only happen with pnpm, but pnpm makes it far more likely, and the wave of reports from 2022 onwards tracks pnpm adoption closely. npm and Yarn flatten node_modules by default, so a transitive dependency generally ends up at the top level where TypeScript can name it in a portable way; you get the error from them only when a version conflict forces a nested copy. pnpm links only the dependencies you actually declared and keeps the rest in its content-addressed store, which is the whole point of the design — but it means a type that reaches your public API through an undeclared package has no name your consumers could use either. Seen that way pnpm is reporting a real portability problem the flat layout was hiding. The fixes are the same in both worlds: annotate the export, or declare the package as a direct dependency so it becomes nameable.
They are the same diagnosis with different amounts of detail. TS2742 tells you which export failed and which module could not be referenced; TS2883 adds the specific symbol the emitter was trying to write, so you get 'QueryBuilder' from 'db-client/node_modules/query-core' instead of just the path. TS2883 arrived in TypeScript 5.x and is what 6.0 emits in most situations where 5.3 and earlier emitted TS2742, so which code you see is mainly a function of your compiler version:
// TS 5.3 → TS2742 · TS 6.0 → TS2883, same line, same fix
export const dbClient: ReturnType<typeof createClient> = createClient()Treat them as one error. Everything on this page — annotate the export, install the transitive package directly, dedupe the tree, or take the file out of declaration emit — applies unchanged to both codes.
Browse all TypeScript practice challenges to keep sharpening your type-level skills.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges