Skip to content

fix(next/image): annotate Image export so deprecated props show as deprecated - #98597

Open
franpeza wants to merge 1 commit into
vercel:canaryfrom
franpeza:fix/image-props-jsdoc-deprecations
Open

fix(next/image): annotate Image export so deprecated props show as deprecated#98597
franpeza wants to merge 1 commit into
vercel:canaryfrom
franpeza:fix/image-props-jsdoc-deprecations

Conversation

@franpeza

Copy link
Copy Markdown

Problem

The @deprecated tags on next/image props don't show up in the editor. If you write <Image priority /> you get no strikethrough and no hint that you should use preload now.

The tags are there in ImageProps, they just never reach the user.

Cause

Image is declared like this, with the return type inferred:

export const Image = forwardRef<HTMLImageElement | null, ImageProps>(...)

ImageProps lives in another file (shared/lib/get-img-props). When TypeScript expands a type alias from a different module during declaration emit, it drops the JSDoc of all the members. If the alias is in the same file the comments survive, cross file they don't.

This is what we ship today:

export declare const Image: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<...>, "src" | "height" | ...> & {
    preload?: boolean;
    priority?: boolean;   // the @deprecated is gone
    ...
} & React.RefAttributes<HTMLImageElement | null>>;

And this is what we get with the fix:

import type { ImageProps } from '../shared/lib/get-img-props';
export declare const Image: React.ForwardRefExoticComponent<ImageProps & React.RefAttributes<HTMLImageElement | null>>;

It recovers the tags for priority, onLoadingComplete, layout, objectFit, objectPosition, lazyBoundary and lazyRoot. So every deprecation added to the component since #83351 has been invisible.

Is this breaking?

No. The old expanded type and the new one are assignable in both directions, and the props you get in autocomplete are the same (304 entries, same list). Only the way the type is printed changes. Now the deprecated ones are also marked as deprecated in the completion list.

The component is split in two (ImageImpl + the annotated export) only to keep the diff small. If I annotate the export directly, Prettier reformats the whole function body and the diff becomes 135 lines for a one line change.

Test

test/unit/image-component-deprecated-props.test.ts compiles a small file that imports next/image, and checks that getJsDocTags() returns deprecated for each of those props. With the old declaration it fails 7 of 8 (the 8th one only checks the props resolve, and passes either way, so it works as a control).

I didn't use expectTypeOf like infer-get-server-side-props-type.test.ts does, because both types are assignable to each other, so a type equality check passes before and after the fix. It can't catch this.

Alternative I tried

Turning ImageProps into an interface also fixes it, and it needs no changes in the component, because interfaces are always referenced by name in the emitted types. I didn't go that way because interfaces don't get an implicit index signature, so const x: Record<string, unknown> = imageProps stops compiling. ImageProps is public, so that would break users. Happy to switch if you prefer it.

Also worth noting: with --isolatedDeclarations TypeScript already rejects the current code with TS9010: Variable must have an explicit type annotation. So this annotation is what TS wants anyway.

…es call sites

`Image` was declared as `forwardRef<HTMLImageElement | null, ImageProps>(...)`
with the return type left to inference. Because `ImageProps` is declared in
another module, declaration emit expands the alias inline and drops the JSDoc of
every member, so the `@deprecated` tags never reach editors at the call site.

Annotating the export explicitly keeps the alias by reference, which preserves
the tags for `priority`, `onLoadingComplete`, `layout`, `objectFit`,
`objectPosition`, `lazyBoundary` and `lazyRoot`.

The emitted type is unchanged in substance: the previous expanded form and
`ImageProps & React.RefAttributes<HTMLImageElement | null>` are mutually
assignable, and prop completion is identical.
@franpeza
franpeza marked this pull request as ready for review September 12, 2026 07:37

@SapanMozammel SapanMozammel left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great TypeScript catch! Explicitly typing Image prevents declaration emit from dropping member JSDoc and @deprecated tags at consumer call sites. Unit tests look solid.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants