mobxjs/mobx · warning

Expected exactly one class declaration for '${target.name}'

Error message

Expected exactly one class declaration for '${target.name}' but found ${declarations.length}

What it means

When the codemod rewrites a `decorate(ClassName, {...})` call it must locate the class declaration bound to that name via scope bindings, to move annotations into it. If zero declarations are found for the identifier, the transform cannot proceed for that call and warns instead.

Source

Thrown at packages/mobx-undecorate/src/undecorate.ts:181

                let canRemoveDecorateCall = true
                if (callPath.value.arguments.length !== 2) {
                    warn("Expected a decorate call with two arguments", callPath.value)
                    return
                }
                const target = callPath.value.arguments[0]
                const decorators = callPath.value.arguments[1]

                if (!j.Identifier.check(target)) {
                    // not targeting a class, just swap it with makeObservable
                    changed = true
                    // @ts-ignore // TODO: or "observable" ?
                    callPath.value.callee.name = "makeObservable"
                    needsInitializeImport = true
                    return
                }
                const declarations = callPath.scope.getBindings()[target.name]
                if (declarations.length === 0) {
                    warn(
                        `Expected exactly one class declaration for '${target.name}' but found ${declarations.length}`,
                        target
                    )
                    return
                }
                const targetDeclaration = declarations[0].parentPath.value
                if (!j.ClassDeclaration.check(targetDeclaration)) {
                    // not targeting a class, just swap it with makeObservable
                    changed = true
                    // @ts-ignore // TODO: or "observable" ?
                    callPath.value.callee.name = "makeObservable"
                    needsInitializeImport = true
                    return
                }
                const clazz: ClassDeclaration = targetDeclaration
                // @ts-ignore
                createConstructor(clazz, decorators, [])
                needsInitializeImport = true

View on GitHub (pinned to 01211a698b)

Solutions

  1. Ensure the decorated class is declared in the same file as the decorate call; move decorate into the class's defining module before running the codemod.
  2. Convert the decorate call manually to `makeObservable(this, {...})` inside the class constructor.
  3. Check for typos in the identifier passed as the first argument.

Example fix

// before (in file A)
import { Store } from './store'
decorate(Store, { count: observable })
// after (move into store.ts constructor)
constructor() { makeObservable(this, { count: observable }) }
Defensive patterns

Strategy: validation

Validate before calling

// ensure the decorated name is a class declared in the same file
const hasLocalClass = /class\s+Store\b/.test(src)
if (src.includes('decorate(Store') && !hasLocalClass) {
  throw new Error('decorate target must be a class declared in the same file')
}

Type guard

function isLocallyDeclaredClass(name, source) {
  return new RegExp(`(class|const|function)\\s+${name}\\b`).test(source)
}

Prevention

When it happens

Trigger: `decorate(SomeName, {...})` where `SomeName` has no resolvable binding in the current scope — e.g. the class is imported (bindings resolve to the import, not a class declaration) or the name is undefined.

Common situations: decorate applied to an imported class from another module, decorate applied to re-exported names, or typos in the class name. The codemod only handles classes declared in the same file.

Related errors


AI-assisted analysis of mobxjs/mobx@01211a698b (2026-08-28). Data as JSON: /api/errors/36c0d743b886d6da. Report an issue: GitHub.