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 { parseMarkdown } 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 parseMarkdown(content, {
plugins: [
highlight({
themes: { light: githubLight, dark: githubDark },
transformers: [transformerTwoslash()],
}),
],
})comark config and it runs at build time, with 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 { Markdown } 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>
<Markdown v-if="plugins" :plugins="plugins">{{ content }}</Markdown>
</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; }