Skip to content

Add native support for twoslash comment queries #52839

@orta

Description

@orta

Suggestion

const hi = "Hi"
const msg = `${hi} world` as const
//    ^?

Should add an inline recommendation to editors what the type is at msg.

I think there are a significant amount of new TypeScript users who are learning the language through video tools/tutorials. Probably much more than we expect. Lots of the folks teaching use this syntax via a vscode extension I wrote a few years back.

I initially punted on discussing internally about upstreaming it into TSServer, but I've slowly turned around on the idea as it feels like the teaching community is changing to be more video focused. So, I think it's worth the complexity, at least to bring it up.

🔍 Search Terms

inline comments show types messages print types twoslash

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

TSServer already has inlay hint support for other features, so it would be expanding support, perhaps that means at parse/bind time checking comments for // ^? and caching their locations, then printing the results when an editor asks for hints in a particular range.

The current code simply uses the same kind of regex as the TSC tests
export function activate(context: vscode.ExtensionContext) {
  const provider: vscode.InlayHintsProvider = {
    provideInlayHints: async (model, iRange, cancel) => {
      const offset = model.offsetAt(iRange.start);
      const text = model.getText(iRange);
      const results: vscode.InlayHint[] = [];

      const m = text.matchAll(/^\s*\/\/\s*\^\?/gm);
      for (const match of m) {
        if (match.index === undefined) {
          return;
        }

        const end = match.index + match[0].length - 1;
        // Add the start range for the inlay hint
        const endPos = model.positionAt(end + offset);
        const inspectionPos = new vscode.Position(
          endPos.line - 1,
          endPos.character
        );

        if (cancel.isCancellationRequested) {
          return [];
        }

        const { scheme, fsPath, authority, path } = model.uri;
        const hint: any = await vscode.commands.executeCommand(
          "typescript.tsserverRequest",
          "quickinfo",
          {
            _: "%%%",
            file: scheme === 'file' ? fsPath : `^/${scheme}/${authority || 'ts-nul-authority'}/${path.replace(/^\//, '')}`,
            line: inspectionPos.line + 1,
            offset: inspectionPos.character,
          }
        );

        if (!hint || !hint.body) {
          continue;
        }

        // Make a one-liner
        let text = hint.body.displayString
          .replace(/\\n/g, " ")
          .replace(/\/n/g, " ")
          .replace(/  /g, " ")
          .replace(/[\u0000-\u001F\u007F-\u009F]/g, "");
        if (text.length > 120) {
          text = text.slice(0, 119) + "...";
        }

        const inlay: vscode.InlayHint = {
          kind: 0,
          position: new vscode.Position(endPos.line, endPos.character + 1),
          label: text,
          paddingLeft: true,
        };
        results.push(inlay);
      }
      return results;
    },
  };

📃 Motivating Example

I think there are 2 main uses for this, and I've framed the initial part of this issue as being about education because that's what changed my opinion. The other part is that it is genuinely useful to pull out types while you are working on a more complex type, in which case twoslash comments act like a way of seeing your work in progress on complex types.

For example when I built a "wordle" in the type system, each type I created I would create an example of it in use and print it in order to see how types flow through the system. You can see a few left in here: https://t.co/OmXoSdi1Y1

Downsides

There are a few interesting downsides to the syntax, which I think are fine but at east worth highlighting:

Extensions

We could also add support for drilling into the types:

type Person = {
  name: string;
  address: {
    line1: string;
    country: string;
  }
}

const person = bob;
//     ^?['address']  { line1: string; country: string }

Metadata

Metadata

Assignees

No one assigned

    Labels

    DeclinedThe issue was declined as something which matches the TypeScript visionDomain: Inlay HintsRevisitAn issue worth coming back toSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions