Why is AppError not assignable to AppError after adding one dependency?
The app stopped compiling right after a new dependency was installed. Both copies of the package @acme/errors are the same version and the class source is unchanged.
Constraints: npm ls @acme/errors prints the package twice — once hoisted at the root, once nested under @acme/client. Do not weaken the code with as any.
// node_modules/@acme/errors/index.d.ts (two copies exist on disk)
export declare class AppError extends Error {
private code: string;
constructor(code: string);
}
// apps/web/src/handler.ts
import { AppError } from '@acme/errors'; // resolves to the hoisted root copy
import { report } from '@acme/client'; // report(e: AppError) — the nested copy
report(new AppError('E_TIMEOUT'));
// error TS2345: Argument of type 'AppError' is not assignable to parameter of type 'AppError'.
// Types have separate declarations of a private property 'code'.
Diagnose the cause.
Two copies of @acme/errors are on disk, so the compiler builds two unrelated AppError types. Structural matching cannot help: a private member makes the class nominal, and private members from separate declarations never match. Deduplicate it.
- ✗Assuming structural typing makes two identically-shaped classes interchangeable, ignoring
private - ✗Blaming a version mismatch when both copies are in fact the same version
- ✗Silencing the error with
as anyinstead of deduplicating the dependency
- →Which command shows you that a package resolved twice in the dependency tree?
- →Why does a
privatefield make an otherwise structural class behave nominally?
The error looks absurd — a type not assignable to itself — but the names are lying. There are two @acme/errors on disk, and the compiler built two independent AppError symbols out of them.
Structural typing normally smooths this over: two identically-shaped types are compatible. But private code makes the class nominal. A private member is compatible only with itself — the same declaration in the same file. Two copies of the file means two different private members, hence two incompatible types.
The cure is deduplication, not typing. Force a single copy to resolve:
{
"overrides": { "@acme/errors": "2.4.1" }
}
Then npm ls @acme/errors must list the package exactly once. In a workspace the same result comes from hoisting the dependency to the root, or from moving @acme/errors into @acme/client's peerDependencies so it stops pulling its own nested copy.