How to use `mdx` and `shiki-twoslash` to add VSCode-like syntax highlighting and TypeScript experience to code blocks
shiki-twoslash
?shiki-twoslash
is a library that combines the functionality of two
libraries:
shiki
: a syntax highlighter that produces HTML that looks just like code
in VS Codetwoslash
: a markup syntax for TypeScript code blocks powered by the
TypeScript compiler (used by the TypeScript website)shiki-twoslash
allows us to produce TypeScript code blocks that are as
beautiful as they are powerful:
ts
constisString = (x : unknown):x is string => typeofx === 'string'constisNumber = (x : unknown):x is number => typeofx === 'number'constisBoolean = (x : unknown):x is boolean => typeofx === 'boolean'declare constthing : unknownif (isString (thing )) {thing } else if (isNumber (thing )) {thing } else if (isBoolean (thing )) {thing }
(Be sure to hover over the code block above to see the TypeScript compiler-powered goodness)
This functionality enables us to bring otherwise boring and static code blocks to life!
Let's take a look at how to add it to your MDX-powered blog and how to use it!
shiki-twoslash
hides a significant amount of complexity behind a few types of
markup. In this section, we'll look at how to use some of them.
By the way, in order to use these twoslash
features, you need to create a code
block inside a ts twoslash
fence:
```ts twoslash// TypeScript compiler-driven code goes here!```
Queries allow us to query the TypeScript compiler for information.
^?
The ^?
query tells the TypeScript compiler to display tooltip information
about the identifier above that ^
is pointing at.
⬇️
```ts twoslashconst person = 'piablo'// ^?```
ts
constperson = 'piablo'
⬇️
```ts twoslashconst addOne = (n: number) => n + 1// ^?```
ts
constaddOne = (n : number) =>n + 1
^|
The ^?
query tells the TypeScript compiler to display an autocomplete dropdown
at the point that ^
points to.
It requires the use of a // @noErrors
comment at the top of the block.
⬇️
```ts twoslash// @noErrorsconst string = 'hello world''hello'.to// ^|```
ts
conststring = 'hello world''hello'.to
Code blocks can display a title using the title
code fence meta attribute:
⬇️
```ts twoslash title="title.ts"const hello = 'world'```
title.tsts
consthello = 'world'
The title can be styled using the .code-title
class.
shiki-twoslash
can highlight specific lines of code by attaching line numbers
to the code fence:
⬇️
```ts twoslash {1, 3}const highlightedLine = {notHighlighted: true,highlighted: true,}```
ts
consthighlightedLine = {notHighlighted : true,highlighted : true,}
shiki-twoslash
can render inline errors produced by the TypeScript compiler.
By default, Shiki will throws an error and displays a debug component for TypeScript compiler errors. This helps protect us from writing incorrect code samples.
For example, if we try to render the following code block:
```ts twoslashconst addOne = (n: number) => n + 1addOne('hello!')```
The code sample would throw a type error (because we can't use hello!
as a
number
):
Errors were thrown in the sample, but not included in an errors tagThese errors were not marked as being expected: 2345.Expected: // @errors: 2345
shiki-twoslash
helpfully explains exactly how to resolve the issue:
// @error: 2345
Let's add that to our code sample:
```ts twoslash// @errors: 2345const addOne = (n: number) => n + 1addOne('hello!')```
And now it renders the error inline:
ts
constaddOne = (n : number) =>n + 1Argument of type 'string' is not assignable to parameter of type 'number'.2345Argument of type 'string' is not assignable to parameter of type 'number'.addOne ('hello!' )
shiki-twoslash
offers the ---cut---
markup which allows us to hide all of
the code before that line in the produced code sample.
This allows us to write correct code (code that can be compiled) while only showing part of the code, or to avoid repeating the same code across multiple code samples.
Here's an example:
⬇️
```ts twoslashinterface User {name: string}// ---cut---const bluePanda: User = {name: 'BluePanda',}```
ts
constbluePanda :User = {name : 'BluePanda',}
This is helpful if you're iteratively telling a story about some code by introducing parts across multiple code blocks:
⬇️ts
// first we define our type...interfaceUser {name : string}
⬇️ts
interfaceUser {name : string}// then we assign it to a variable...constuser :User = {name : 'BluePanda'}
ts
interfaceUser {name : string}constuser :User = {name : 'BluePanda'}// now we have a user with a `name`!console .log (user .name )
Did we really need to include the definition for User
in every code block?
To make the compiler happy? Yes.
For our readers? Nah.
If we used ---cut---
instead:
```ts twoslashinterface User {name: string}const user: User = {name: 'BluePanda'}// ---cut---// now we have a user with a `name`!console.log(user.name)```
The compiler is happy and we only see the relevant code:
ts
// now we have a user with a `name`!console .log (user .name )
If you think ---cut---
is useful, wait till you see the next section!
shiki-twoslash
gives us the ability to create virtual code blocks that can be
fully or partially re-used within other code blocks.
To reuse a block, use twoslash include <name>
(notice that lack of ts
) in
the block's code fence:
```twoslash include boxinterface Box<T> {thing: T}```
Then reference the block by name using @include
in a ts twoslash
block:
```ts twoslash// @include: box```
If used correctly, we see the Box
definition:
ts
interfaceBox <T > {thing :T }
shiki-twoslash
treats the included code like it's part of the code block it's
imported into!
This means we can use the included code and ---cut---
to make the compiler
happy without showing the included code.
⬇️
```ts twoslash// @include: box// ---cut---const box: Box<string> = {thing: 'hello!'}```
ts
constbox :Box <string> = {thing : 'hello!'}
We can also include only part of a code sample by using // - <location-name>
⬇️
```twoslash include maintype Scream<T extends string> = Uppercase<T>// - screamtype Whisper<T extends string> = Lowercase<T>// - both``````ts twoslash// @include: main-scream``````ts twoslash// @include: main-both```
ts
typeScream <T extends string> =Uppercase <T >
ts
typeScream <T extends string> =Uppercase <T >typeWhisper <T extends string> =Lowercase <T >