---
title: "twoslash"
description: "The same snippets, rendered twice: once by `@twinkleplop/twoslash`, once by `@shikijs/twoslash` with its rich renderer. Hover a marked identifier in either pane."
source: https://twinkleplop.pngwn.at/twoslash
---
# twoslash

The same snippets, rendered twice: once by `@twinkleplop/twoslash`, once by `@shikijs/twoslash` with its rich renderer. Hover a marked identifier in either pane.

Both sides run the same twoslash — same compiler options, same vfs root, same custom tags — so the type information behind the two panes is identical and what differs is the markup and the stylesheet each package ships. Each pane is styled by its own package's CSS, unmodified apart from dark-mode popup colours for shiki, which `style-rich.css` leaves to the host. The light/dark switch in the header repaints both.

- twinkleplop nests `.twoslash-popover` inside the hover target; shiki nests `.twoslash-popup-container`, and re-highlights the type string with the TypeScript grammar rather than tokenizing it directly.
- Queries are a block annotation after the line (`.twoslash-query`) against a popup pinned open (`.twoslash-query-persisted`).
- shiki's rich renderer draws icons for custom tags and completions; twinkleplop emits the tag name from CSS and no icons.
- Neither block scrolls here, because an `overflow` on an ancestor clips the popovers in both. On phones they scroll, and the popovers do get cut off.

jump [Hover types](https://twinkleplop.pngwn.at/twoslash#hover)·[Query (^?)](https://twinkleplop.pngwn.at/twoslash#query)·[Expected errors](https://twinkleplop.pngwn.at/twoslash#errors)·[Type-level programming](https://twinkleplop.pngwn.at/twoslash#types)·[Custom tags](https://twinkleplop.pngwn.at/twoslash#tags)·[Highlight (^^^)](https://twinkleplop.pngwn.at/twoslash#highlight)·[Completions (^|)](https://twinkleplop.pngwn.at/twoslash#completions)·[Setup above the cut](https://twinkleplop.pngwn.at/twoslash#cut) ·[Svelte](https://twinkleplop.pngwn.at/twoslash#svelte)

## Hover types

Every identifier carries the type TypeScript inferred for it. Hover a marked identifier in either pane to see it.

twinkleplop @twinkleplop/twoslash

```text
function greet(name: string, loud: boolean) {
  const suffix = loud ? "!!!" : ".";
  return `Hello, ${name}${suffix}`;
}

const message = greet("world", true);
```

shiki @shikijs/twoslash · rendererRich

```text
function function greet(name: string, loud: boolean): stringgreet(name: stringname: string, loud: booleanloud: boolean) {
  const const suffix: "!!!" | "."suffix = loud: booleanloud ? "!!!" : ".";
  return `Hello, ${name: stringname}${const suffix: "!!!" | "."suffix}`;
}

const const message: stringmessage = function greet(name: string, loud: boolean): stringgreet("world", true);
```

## Query (^?)

A // ^? comment asks twoslash to print the type of the token above the caret. twinkleplop inserts a block annotation, which pushes the rest of the snippet down; shiki's default queryRendering is an absolutely positioned popup pinned open, which reserves no space and so lies over the lines below it.

twinkleplop @twinkleplop/twoslash

```text
const point = { x: 1, y: 2 };
const point: {
    x: number;
    y: number;
}
const tags = ["admin", "editor"] as const;
const tags: readonly ["admin", "editor"]
```

shiki @shikijs/twoslash · rendererRich

```text
const const point: {
    x: number;
    y: number;
}point = { x: numberx: 1, y: numbery: 2 };

const const tags: readonly ["admin", "editor"]tags = ["admin", "editor"] as type const = readonly ["admin", "editor"]const;
```

## Expected errors

Declare expected diagnostics with // @errors: <code>. The offending range is decorated and the message is rendered beneath the line.

twinkleplop @twinkleplop/twoslash

```text
const count: number = "three";
Type 'string' is not assignable to type 'number'.
function square(n: number) {
  return n * n;
}

const result = square("four");
Argument of type 'string' is not assignable to parameter of type 'number'.
```

shiki @shikijs/twoslash · rendererRich

```text
const count: number = "three";
Type 'string' is not assignable to type 'number'.
function function square(n: number): numbersquare(n: numbern: number) {
  return n: numbern * n: numbern;
}

const const result: numberresult = function square(n: number): numbersquare("four");
Argument of type 'string' is not assignable to parameter of type 'number'.
```

## Type-level programming

Inferred types for type-level constructs surface the same way value types do: twoslash reports the resolved alias and both renderers print it against the query. The type string inside a popover is highlighted too — twinkleplop tokenizes it with its own grammar, shiki runs it back through codeToHast.

twinkleplop @twinkleplop/twoslash

```text
type Flatten<T> = T extends Array<infer U> ? U : T;

type A = Flatten<string[]>;
type A = string
type B = Flatten<number>;
type B = number
```

shiki @shikijs/twoslash · rendererRich

```text
type type Flatten<T> = T extends (infer U)[] ? U : TFlatten<function (type parameter) T in type Flatten<T>T> = function (type parameter) T in type Flatten<T>T extends interface Array<T>Array<infer function (type parameter) UU> ? function (type parameter) UU : function (type parameter) T in type Flatten<T>T;

type type A = stringA = type Flatten<T> = T extends (infer U)[] ? U : TFlatten<string[]>;

type type B = numberB = type Flatten<T> = T extends (infer U)[] ? U : TFlatten<number>;
```

## Custom tags

@log, @annotate, @warn and @error render as line annotations. shiki's rich renderer prefixes each with an icon; twinkleplop prefixes the tag name from CSS.

twinkleplop @twinkleplop/twoslash

```text
const config = { retries: 3, timeout: 500 };
loading the config
const retries = Math.max(0, config.retries);
clamped so a negative count cannot disable retries
const timeout = config.timeout;
timeout is in milliseconds, not seconds
```

shiki @shikijs/twoslash · rendererRich

```text
const const config: {
    retries: number;
    timeout: number;
}config = { retries: numberretries: 3, timeout: numbertimeout: 500 };loading the config
const const retries: numberretries = var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.Math.Math.max(...values: number[]): numberReturns the larger of a set of supplied numeric expressions.@paramvalues Numeric expressions to be evaluated.max(0, const config: {
    retries: number;
    timeout: number;
}config.retries: numberretries);clamped so a negative count cannot disable retries
const const timeout: numbertimeout = const config: {
    retries: number;
    timeout: number;
}config.timeout: numbertimeout;timeout is in milliseconds, not seconds
```

## Highlight (^^^)

A // ^^^ comment marks a range of the line above for emphasis.

twinkleplop @twinkleplop/twoslash

```text
const enabled = true;
```

shiki @shikijs/twoslash · rendererRich

```text
const const enabled: trueenabled = true;
```

## Completions (^|)

A // ^| comment asks for the completion list at that position. Both render it as a dropdown anchored to the caret. shiki splits each entry into the typed prefix and the rest, and draws an icon for the member kind; twinkleplop emits the name whole, with the kind on data-kind and the typed prefix on the host's data-prefix, so either is there to style.

twinkleplop @twinkleplop/twoslash

```text
const users = ["ada", "grace"];
users.fi
fillfilterfindfindIndex

Property 'fi' does not exist on type 'string[]'.
```

shiki @shikijs/twoslash · rendererRich

```text
const const users: string[]users = ["ada", "grace"];
const users: string[]users.fifillfilterfindfindIndex
Property 'fi' does not exist on type 'string[]'.
```

## Setup above the cut

Everything above a // ---cut--- line is type checked but not rendered, so a snippet can lean on declarations the reader never sees.

twinkleplop @twinkleplop/twoslash

```text
const size = raw.length;
```

shiki @shikijs/twoslash · rendererRich

```text
const const size: numbersize = const raw: stringraw.String.length: numberReturns the length of a String object.length;
```

## Svelte

`@twinkleplop/twoslash-svelte` runs components through `svelte2tsx`, hands the generated TSX to the same TypeScript language service, and maps hover, query and error positions back onto the original Svelte source. There is no shiki counterpart to put beside it, so these are one pane wide.

### Svelte 5 runes + template bindings

Svelte components flow through svelte2tsx so twoslash sees the same TypeScript svelte-language-server does. Hovers work on runes, script identifiers, AND template expressions like {count}.

twinkleplop @twinkleplop/twoslash-svelte

```text
<script lang="ts">
	let count = $state(0);

	function increment() {
		count += 1;
	}
</script>

<button onclick={increment}>
	clicks: {count}
</button>
```

### Query (^?) in a Svelte script

The ^? marker works inside the <script> block the same way it does in plain TypeScript.

twinkleplop @twinkleplop/twoslash-svelte

```text
<script lang="ts">
	const user = { name: "alice", age: 30, admin: true };
const user: {
    name: string;
    age: number;
    admin: boolean;
}
</script>
```

### Expected errors in Svelte

Declare diagnostics with // @errors: at column 0 inside the script block. Typing a number into a string-annotated const gets a proper squiggle and message.

twinkleplop @twinkleplop/twoslash-svelte

```text
<script lang="ts">
const label: string = 42;
Type 'number' is not assignable to type 'string'.
</script>
```

[docs / twoslash](https://twinkleplop.pngwn.at/docs/twoslash) documents the API these panes exercise. For token-level highlighting against shiki — live, editable, timed — see [explore](https://twinkleplop.pngwn.at/explore).
