---
title: "twoslash"
description: "TypeScript type information rendered into the highlighted output."
source: https://twinkleplop.pngwn.at/docs/twoslash
---
# twoslash

TypeScript type information rendered into the highlighted output.

Twoslash is a gift, and as with all gifts, it is rude to refuse.

Twoslash uses the TypeScript compiler to get hover types, query results, completions and errors for a snippet. Twinkleplop includes this information in the highlighted HTML.

Every notation below is rendered side by side with `@shikijs/twoslash` on the [twoslash comparison page](https://twinkleplop.pngwn.at/twoslash).

## install and use

```text title="terminal"
pnpm add @twinkleplop/twoslash
```

`create_highlighter` builds a reusable highlight function, the same shape as a language package's.

```ts title="twoslash.ts"
import { create_highlighter } from "@twinkleplop/twoslash";

const highlight = create_highlighter({ lang: "ts" });
const html = highlight(code);
```

> Create the highlighter once and reuse it. Each one builds its own TypeScript environment, which costs far more than highlighting a snippet, so calling `create_highlighter` per snippet can make a docs build several times slower.

Use `@twinkleplop/twoslash-svelte` for Svelte snippets. It takes the same options except `lang`.

```ts title="svelte.ts"
import { create_highlighter } from "@twinkleplop/twoslash-svelte";

// same options, minus `lang`
const highlight = create_highlighter();
```

The output needs two stylesheets. A theme colours the tokens; `@twinkleplop/twoslash/style.css` positions the twoslash spans. Without it every popover renders inline, so hover type text appears in the middle of the code instead of on hover.

```ts title="styles.ts"
// token colours, then the twoslash layout/visibility rules
import "@twinkleplop/theme-github";
import "@twinkleplop/twoslash/style.css";
```

> The popovers are positioned against their hover target, which an ancestor with `overflow: hidden` or `overflow: auto` clips. If your code blocks scroll, drive them with the Popover API instead: `.twoslash-popover` sits directly inside `.twoslash-hover`, so setting `popover="manual"` and calling `showPopover()` on pointerover promotes them to the top layer.

## custom tags

Twoslash throws an error for unregistered tags such as `// @foo`. Four tags are registered by default: `annotate`, `log`, `warn` and `error`.

```ts title="input.ts"
// @log: x is 1
const x = 1;
// @annotate: y follows x
const y = x + 1;
```

```ts title="output.ts"
const x = 1;
x is 1
const y = x + 1;
y follows x
```

A tag renders beneath the line that follows it. Tags on adjacent lines merge into one, so keep a line of code between them.

Pass `custom_tags` to replace that list, or `[]` to disable tags.

## options

```ts title="options.ts"
import { marked } from "marked";
import { create_highlighter } from "@twinkleplop/twoslash";
import { language as plain_ts } from "@twinkleplop/typescript";

const fallback = plain_ts();

const highlight = create_highlighter({
  lang: "ts",
  // a snippet twoslash rejects falls back to plain highlighting
  on_error: (_error, code) => fallback(code),
  // jsdoc becomes rendered markdown instead of escaped text
  render_docs: (md) => marked.parseInline(md, { async: false }),
  // massage the type string before it is highlighted
  process_type: (type) => type.replace(/import\(".*?"\)\./g, ""),
});
```

| option | type | default | meaning |
| --- | --- | --- | --- |
| lang | "ts" \| "tsx" \| "js" \| "jsx" | "ts" | Which grammar to tokenize with. |
| class_name | string | "twinkleplop twoslash" | Replaces the class on the block. |
| custom_tags | string[] | annotate, log, warn, error | Tags usable as `// @<tag>: text`. |
| on_error | (error, code) => string \| void | — | Called when twoslash rejects the snippet. A string return becomes the result; no return rethrows. |
| render_docs | (markdown) => string | escaped | Renders jsdoc. The return value is inserted as trusted HTML. |
| process_type | (type) => string | identity | Called with every hover and query type before highlighting. |
| docs_tags | "split" \| "raw" | "split" | `split` gives each jsdoc tag its own element; `raw` keeps one escaped block. |
| escape | Record<string, string> | — | Extra characters to encode, as in render options. Covers the code, types, docs and tags but not `render_docs` output. |
| twoslash | TwoslashOptions | — | Passed through to twoslash itself. |

> `render_docs` output is inserted as trusted HTML. Sanitise it yourself if the jsdoc is untrusted. Without the option, docs are escaped text. `escape` does not reach that HTML, so encode the same characters in what `render_docs` returns.

`on_error` handles errors from Twoslash. Tokenization and rendering errors are thrown directly.

## in markdown

A language registry entry can include a separate Twoslash highlighter. Fences marked ```` ```ts twoslash ```` use it; ```` ```ts ```` fences use the regular highlighter.

```ts title="markdown.ts"
import rehype_twinkleplop from "@twinkleplop/rehype";
import { create_highlighter } from "@twinkleplop/twoslash";
import { language as typescript } from "@twinkleplop/typescript";

.use(rehype_twinkleplop, {
  languages: {
    ts: {
      highlight: typescript(),
      twoslash: create_highlighter({ lang: "ts" }),
    },
  },
});
```

Set `twoslash: "always"` in the plugin options to route every fence through the twoslash highlighter instead. See [markdown](https://twinkleplop.pngwn.at/docs/markdown).
