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:
tsconstisString = (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'// ^?```
tsconstperson = 'piablo'
⬇️```ts twoslashconst addOne = (n: number) => n + 1// ^?```
tsconstaddOne = (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// ^|```
tsconststring = '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.tstsconsthello = '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,}```
tsconsthighlightedLine = {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:
tsconstaddOne = (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',}```
tsconstbluePanda :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}
⬇️tsinterfaceUser {name : string}// then we assign it to a variable...constuser :User = {name : 'BluePanda'}
tsinterfaceUser {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:
tsinterfaceBox <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!'}```
tsconstbox :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```
tstypeScream <T extends string> =Uppercase <T >
tstypeScream <T extends string> =Uppercase <T >typeWhisper <T extends string> =Lowercase <T >