Twoslash
Twoslash runs the real TypeScript compiler on your code blocks to produce inline type tooltips, expected error markers, and hidden setup code — all at parse time, with zero client-side JavaScript.
It is implemented as a Shiki transformer and wires into Comark through the highlight plugin.
Installation
pnpm add shiki @shikijs/twoslashnpm install shiki @shikijs/twoslashyarn add shiki @shikijs/twoslashbun add shiki @shikijs/twoslashpnpm add shiki @shikijs/twoslash twoslash-cdnnpm install shiki @shikijs/twoslash twoslash-cdnyarn add shiki @shikijs/twoslash twoslash-cdnbun add shiki @shikijs/twoslash twoslash-cdnServer
In a Node.js context — SSR, static site generation, or a build-time plugin — TypeScript's own file system is available, so use @shikijs/twoslash directly:
import { parse } from 'comark'
import highlight from 'comark/plugins/highlight'
import { transformerTwoslash } from '@shikijs/twoslash'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
const result = await parse(content, {
plugins: [
highlight({
themes: { light: githubLight, dark: githubDark },
transformers: [transformerTwoslash()],
}),
],
})comark config and it runs at build time — zero client JavaScript.Browser
In the browser there is no filesystem, so TypeScript cannot load its type definitions the normal way. Use twoslash-cdn to fetch them over CDN instead:
import { createTransformerFactory, rendererRich } from '@shikijs/twoslash/core'
import { createTwoslashFromCDN } from 'twoslash-cdn'
import highlight from 'comark/plugins/highlight'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
const twoslash = createTwoslashFromCDN()
await twoslash.init()
const transformer = createTransformerFactory(twoslash.runSync)({
explicitTrigger: true,
renderer: rendererRich(),
})
const plugin = highlight({
themes: { light: githubLight, dark: githubDark },
transformers: [transformer],
})explicitTrigger: true limits compilation to code blocks tagged with twoslash in their meta string — plain ts blocks are left untouched. Pair with @shikijs/twoslash/style-rich.css for the default popup styles.Vue + Vite
<script setup lang="ts">
import { shallowRef, onMounted } from 'vue'
import { Comark } from '@comark/vue'
import highlight from '@comark/vue/plugins/highlight'
import { createTransformerFactory, rendererRich } from '@shikijs/twoslash/core'
import { createTwoslashFromCDN } from 'twoslash-cdn'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
import '@shikijs/twoslash/style-rich.css'
import type { ComarkPlugin } from 'comark'
const plugins = shallowRef<ComarkPlugin[] | null>(null)
onMounted(async () => {
const twoslash = createTwoslashFromCDN()
await twoslash.init()
const transformer = createTransformerFactory(twoslash.runSync)({
explicitTrigger: true,
renderer: rendererRich(),
})
plugins.value = [
highlight({
themes: { light: githubLight, dark: githubDark },
transformers: [transformer],
}),
]
})
</script>
<template>
<Suspense>
<Comark v-if="plugins" :plugins="plugins">{{ content }}</Comark>
</Suspense>
</template>Annotations
Add annotations directly in your code block — they are compiled and stripped from the output:
| Annotation | Where | Effect |
|---|---|---|
^? | After a symbol | Show inferred type in a hover popup |
// @errors: N … | Top of block | Expect TypeScript error codes; mark others as unexpected |
// @noErrors | Top of block | Suppress all type errors silently |
// ---cut--- | Any line | Everything above is compiled but hidden from output |
// ---cut-after--- | Any line | Everything below is compiled but hidden from output |
// @filename: foo.ts | Top of block | Treat block as a named virtual file (for multi-file examples) |
Type Hover · ^?
Point an arrow at any identifier to show its inferred type in a popup:
```ts twoslash
const message = "Hello from Twoslash"
// ^?
```Error Annotations · @errors
Document intentional type errors — great for teaching correct API usage:
```ts twoslash
// @errors: 2322
let count: number = "not a number"
```Hide Setup · // ---cut---
Code above the cut compiles but is hidden from readers — useful for imports and shared types:
```ts twoslash
interface User { id: number; name: string }
function getUser(id: number): User {
return { id, name: 'Alice' }
}
// ---cut---
const user = getUser(42)
// ^?
```Styling
Import the bundled stylesheet for popup styles, then override variables to match your theme:
import '@shikijs/twoslash/style-rich.css'Dark mode overrides (using Nuxt UI CSS variables as an example):
.dark .twoslash-popup-container {
background: var(--ui-bg-elevated) !important;
border-color: var(--ui-border) !important;
}
.dark .twoslash-popup-code span {
color: var(--shiki-dark) !important;
}
/* Prevent nested <pre> inside popup from inheriting block styles */
pre .twoslash-hover pre {
margin: 0;
padding: 0;
background: transparent !important;
border: none;
}
pre .twoslash-hover pre .line { display: inline; }