Skip to content

Conversation

@som-sm
Copy link
Collaborator

@som-sm som-sm commented Dec 5, 2025

This PR validates the types specified via twoslash arrow comments (//=>) inside JSDoc codeblocks using the getQuickInfoAtPosition API, with automatic fixes for incorrect types.

image


It also improves the DX, since we no longer have to manually write the twoslash types, we can simply start the twoslash comment and rely on autofix to fill in the type.

twoslash-autofix-demo.mov

Surprisingly, there were only a few types that were incorrect:
  1. Should be boolean instead of false.

    // `exactOptionalPropertyTypes` disabled
    type B = AllExtend<[1?, 2?, 3?], number>;
    //=> false

  2. Should be true instead of unknown.

    image
  3. Return type is not number.

    get({[symbolValue]: 1} as const, symbolValue);
    //=> number

  4. The resulting type is Infinity and not PositiveInfinity or NegativeInfinity.

    image image

Working:

  1. Twoslash arrow comments (//=>) inside JSDoc codeblocks are identified, and the first position on the previous line where quickinfo is available is retrieved using the getQuickInfoAtPosition API.

  2. The type portion is extracted from the retrieved quickinfo. For example, if the quickinfo is type T1 = string | number;, the extracted type is string | number.

  3. The extracted type is then normalized to match our linting style. This includes converting double-quoted string literals to single-quoted ones and replacing four-space indentation with tabs.

  4. If the normalized type does not match the specified twoslash comment, an autofixable lint error is reported at the appropriate location.


Notes:

  1. Adds support for overwriting default compiler options, like

    ```
    // @exactOptionalPropertyTypes: true
    import type {AllExtend} from 'type-fest';
    type A = AllExtend<[1?, 2?, 3?], number>;
    //=> true
    ```
    ```
    // @exactOptionalPropertyTypes: false
    import type {AllExtend} from 'type-fest';
    type A = AllExtend<[1?, 2?, 3?], number>;
    //=> boolean
    // `exactOptionalPropertyTypes` disabled
    type B = AllExtend<[1?, 2?, 3?], number | undefined>;
    //=> true
    ```

    This is similar to how we'd do it in TS playground.

  2. Catches formatting issues like use of spaces instead of tabs, use of double quotes etc.

    image
  3. For object types, type annotations were previously specified inconsistently, sometimes on a single line and sometimes across multiple lines. This PR makes this behaviour deterministic. If the quickinfo type is within a defined length threshold, it must be written in a single line, otherwise, it must be written just as it appears in the quickinfo.

    Screenshot 2025-12-20 at 2 27 26 PM Screenshot 2025-12-20 at 2 36 33 PM
  4. Twoslash arrow comments (//=>) should now only be used to specify types. All non-type usages (such as error comments) have been converted to plain comments.

    image image

Future improvements:

The order of unions in quickinfo is sometimes messed up, which leads to changes like these:

image image

image

I'll look into this separately if this can be improved.

Fixed by #1320.

@som-sm som-sm force-pushed the feat/validate-twoslash-type-hints-in-codeblocks branch from 1099024 to 95ce69a Compare December 7, 2025 06:59
Repository owner deleted a comment from claude bot Dec 19, 2025
Repository owner deleted a comment from claude bot Dec 19, 2025
@som-sm som-sm force-pushed the feat/validate-twoslash-type-hints-in-codeblocks branch 3 times, most recently from b0ff62d to f5a80f8 Compare December 20, 2025 06:41
Repository owner deleted a comment from claude bot Dec 20, 2025
Repository owner deleted a comment from claude bot Dec 20, 2025
@som-sm som-sm force-pushed the feat/validate-twoslash-type-hints-in-codeblocks branch from 82f96ec to 0289f95 Compare December 20, 2025 08:25
Repository owner deleted a comment from claude bot Dec 20, 2025
@som-sm som-sm force-pushed the feat/validate-twoslash-type-hints-in-codeblocks branch from 0289f95 to 71dde37 Compare December 20, 2025 08:33
Repository owner deleted a comment from claude bot Dec 20, 2025
@som-sm som-sm force-pushed the feat/validate-twoslash-type-hints-in-codeblocks branch from 71dde37 to 218534f Compare December 20, 2025 09:39
Comment on lines 76 to +83
type OctalInteger = Integer<0o10>;
//=> 0o10
//=> 8
type BinaryInteger = Integer<0b10>;
//=> 0b10
//=> 2
type HexadecimalInteger = Integer<0x10>;
//=> 0x10
//=> 16
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Looks like non-decimal base types are automatically converted into decimal base.

image

@sindresorhus
Copy link
Owner

This PR is ready now. All the changes within the source directory are inside JSDoc example codeblocks only, except for the following two changes where the response had to be wrapped in Simplify.

Maybe add a short code comment on why Simplify is needed in those places.

type Age = IntClosedRange<0, 120>;
//=> 0 | 1 | 2 | ... | 119 | 120
type Age = IntClosedRange<0, 20>;
//=> 0 | 1 | 20 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19
Copy link
Owner

@sindresorhus sindresorhus Dec 22, 2025

Choose a reason for hiding this comment

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

This is not ideal, and I bet users will wonder about this and open issues.

Some ideas:

  • Sort the output
  • Some kind of code comment to disable linting for the code block or certain lines.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, let me look into this then, I had implemented a sorting logic earlier, but it was fairly complicated, so I removed it. I’ll try to add it back with proper tests.

Repository owner deleted a comment Dec 22, 2025
@som-sm som-sm force-pushed the feat/validate-twoslash-type-hints-in-codeblocks branch from ac41e8e to 480f7f9 Compare December 22, 2025 07:02
@som-sm
Copy link
Collaborator Author

som-sm commented Dec 22, 2025

why Simplify is needed in those places.

  1. The first Simplify, used in RequiredDeep, ensures that the internal helper type RequiredObjectDeep doesn't leak into the output type.

    Without Simplify, RequiredObjectDeep appears in the output, which is confusing because it is an internal implementation detail that users are not expected to be aware of.
    image

    And, this is flagged by this lint rule because, in this case, the specified twoslash type doesn't include RequiredObjectDeep, whereas the quickinfo type does.

    type RequiredSettings = RequiredDeep<Settings>;
    //=> {
    // textEditor: {
    // fontSize: number;
    // fontColor: string;
    // fontWeight: number | undefined;
    // };
    // autocomplete: boolean;
    // autosave: boolean | undefined;
    // }

    With Simplify in place, there's no RequiredObjectDeep in the output.
    image

  2. In the second case, it's not an internal type that shows up in the output, but the built-in Omit type. In my opinion, it is acceptable for built-in utility types to appear in the output because they have a standard, well-understood meaning.

    This is flagged by this lint rule because, in this case, the specified twoslash type doesn't include Omit, so one possible fix here would have been to update the twoslash type itself, for example:

        type Info2 = {
    	    address: [
    		    {
    			    street: string;
    		    },
    		    {
    			    street2: string;
    			    foo: string;
    		    },
    	    ];
        };
    
        type AddressInfo = OmitDeep<Info2, 'address.1.foo'>;
    -	//=> {address: [{street: string}, {street2: string}]}
    +	//=> {
    +	// 	address: [{
    +	// 		street: string;
    +	// 	}, Omit<{
    +	// 		street2: string;
    +	// 		foo: string;
    +	// 	}, 'foo'>];
    +	// }

    However, I felt it was better to apply Simplify to the output, as it makes the output more readable.


Maybe add a short code comment on

Umm...not sure if a code comment is really necessary here, since Simplify is already used in several places in the codebase to address similar issues. That said, if you still think a comment would be helpful, I’ll add a link to this comment.

@sindresorhus
Copy link
Owner

Umm...not sure if a code comment is really necessary here, since Simplify is already used in several places in the codebase to address similar issues. That said, if you still think a comment would be helpful, I’ll add a link to this comment.

Up to you. Not important. Just thought something like:

We use Simplify to prevent RequiredObjectDeep from appearing in the type hint.

Repository owner deleted a comment from claude bot Dec 23, 2025
@som-sm som-sm force-pushed the feat/validate-twoslash-type-hints-in-codeblocks branch from 3daf895 to a5a6a22 Compare December 25, 2025 05:00
Repository owner deleted a comment from claude bot Dec 25, 2025
Repository owner deleted a comment from claude bot Dec 25, 2025
Repository owner deleted a comment from claude bot Dec 25, 2025
@som-sm som-sm force-pushed the feat/validate-twoslash-type-hints-in-codeblocks branch 2 times, most recently from c767fae to 8f5e261 Compare December 25, 2025 06:06
Repository owner deleted a comment from claude bot Dec 25, 2025
@som-sm som-sm force-pushed the feat/validate-twoslash-type-hints-in-codeblocks branch from 8f5e261 to 5265b61 Compare December 25, 2025 06:19
Repository owner deleted a comment from claude bot Dec 25, 2025
@som-sm som-sm marked this pull request as draft December 25, 2025 06:21
@som-sm som-sm marked this pull request as ready for review December 25, 2025 06:26
@sindresorhus sindresorhus merged commit fe88ad8 into main Dec 25, 2025
10 of 11 checks passed
@sindresorhus sindresorhus deleted the feat/validate-twoslash-type-hints-in-codeblocks branch December 25, 2025 18:08
@sindresorhus
Copy link
Owner

Looks great! Really nice to be able to enforce this. Merry Xmas!

@som-sm
Copy link
Collaborator Author

som-sm commented Dec 25, 2025

Merry Christmas and happy holidays!

type E = Subtract<PositiveInfinity, 9999>;
//=> PositiveInfinity
//=> Infinity
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice improvements. Interestingly it seems like there is no Infinity type. Only a detail that might sound misleading.

Reference:

/**
Matches the hidden `Infinity` type.
Please upvote [this issue](https://github.com/microsoft/TypeScript/issues/32277) if you want to have this type as a built-in in TypeScript.
@see {@link NegativeInfinity}
@category Numeric
*/
// See https://github.com/microsoft/TypeScript/issues/31752
// eslint-disable-next-line no-loss-of-precision
export type PositiveInfinity = 1e999;

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@mrazauskas Yeah, but I think showing PositiveInfinity is not great either because the quickinfo tooltip doesn't show that.

image

So, I think neither option is perfect, I just went with the one that actually shows up in the tooltip. Not sure why quickinfo decides to show Infinity when it doesn’t exist.

Copy link
Contributor

@mrazauskas mrazauskas Jan 4, 2026

Choose a reason for hiding this comment

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

Thanks for clarifying. Now I think this could be reported as bug to TypeScript. Only a detail, but it is rather odd to see quickinfo with types that do not exist. Feels like the internals are leaking here. Seems like this should be flag as a limitation of TypeScript.

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.

4 participants