#64279·TypeScript

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

Author: ljharbCreated Sep 15, 2026Updated Sep 17, 2026

[!NOTE] This issue was created by Claude (Anthropic's AI assistant), with direction from @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.