Skip to content

JSDoc @type on a function: the type in a type predicate is never checked (unused @import reported, missing names not reported) #64279

Description

Note

This issue was created by Claude (Anthropic's AI assistant), with direction from Jordan Harband (@ljharb). All compiler output below is from actual tsc runs.

🔎 Search Terms

"declared but never used" jsdoc, TS6196, TS6133, TS2304, @import unused, jsdoc type predicate unused import, jsdoc type predicate "Cannot find name", @type function declaration type predicate, asserts jsdoc, noUnusedLocals checkJs, FullSignature

🕗 Version & Regression Information

  • This changed between versions 6.0.3 and 7.0.2 (also reproduces on 7.1.0-dev.20260915.1)

⏯ Playground Link

No response (the repro needs a second file for the @import, plus a tsconfig)

💻 Code

// tsconfig.json
{
	"compilerOptions": {
		"module": "nodenext",
		"allowJs": true,
		"checkJs": true,
		"noEmit": true,
		"noUnusedLocals": true
	}
}
// types.d.mts
export type Foo = Error & { code: 'X' };
// isFoo.mjs
/** @import { Foo } from './types.d.mts' */

/** @type {(e: unknown) => e is Foo} */
export function isFoo(e) {
	return e instanceof Error && 'code' in e && e.code === 'X';
}

🙁 Actual behavior

isFoo.mjs(1,15): error TS6196: 'Foo' is declared but never used.

Foo is used, in the e is Foo type predicate of the @type tag.

🙂 Expected behavior

No error, as in 6.0.3.

Additional information about the issue

Each form below was tested in its own file with the same @import and tsconfig:

Form 7.1.0-dev.20260915.1 7.0.2 6.0.3
@type {(e: unknown) => e is Foo} on a function declaration (above) TS6196 TS6196 no error
@type {(e: unknown) => asserts e is Foo} on a function declaration TS6196 TS6196 no error
export default /** @type {(e: unknown) => e is Foo} */ (e) => … TS6196 TS6196 TS6133
@param {unknown} e + @returns {e is Foo} no error no error TS6133
JSDoc cast: /** @type {(e: unknown) => e is Foo} */ ((e) => …) no error no error no error
@type {(e: Foo) => boolean} (Foo as a parameter type) no error no error no error
the first form, plus a call in the same file that narrows with it no error no error no error

Workaround: drop the @import and write e is import('./types.d.mts').Foo.

The predicate's type doesn't seem to be checked at all: names that don't exist aren't reported there either, while the same names in the parameter or return type are (same tsconfig, no imports):

// unchecked.mjs
/** @type {(e: unknown) => e is Missing1} */
export function a(e) {
	return !!e;
}

/** @type {(e: unknown) => asserts e is Missing2} */
export function b(e) {
	if (!e) {
		throw new TypeError();
	}
}

/** @type {(e: Missing3) => boolean} */
export function c(e) {
	return !!e;
}

/** @type {(e: unknown) => Missing4} */
export function d(e) {
	return /** @type {never} */ (e);
}

7.1.0-dev.20260915.1 and 7.0.2:

unchecked.mjs(13,16): error TS2304: Cannot find name 'Missing3'.
unchecked.mjs(18,28): error TS2304: Cannot find name 'Missing4'.

6.0.3:

unchecked.mjs(1,33): error TS2304: Cannot find name 'Missing1'.
unchecked.mjs(6,41): error TS2304: Cannot find name 'Missing2'.
unchecked.mjs(13,16): error TS2304: Cannot find name 'Missing3'.
unchecked.mjs(18,28): error TS2304: Cannot find name 'Missing4'.

Possibly relevant, but I haven't confirmed it: when a @type tag supplies a function's FullSignature, the checker only uses it for the argument-count check (checker.go#L3464-L3468, checker.go#L10320-L10324) and never runs checkSourceElement on it. If so, the predicate's type is never checked or marked as referenced unless something else resolves the predicate, like the same-file call in the last row of the table. #64052 (the open fix for #63754) edits these same lines, but only to strip undefined from the @type's type, so it wouldn't change this.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions