TypeScript TwoSlash
A markup format for TypeScript code, ideal for creating self-contained code samples which let the TypeScript compiler do the extra leg-work. Inspired
by the fourslash test system.
Used as a pre-parser before showing code samples inside the TypeScript website and to create a standard way for us
to create examples for bugs on the compiler's issue tracker.
You can preview twoslash on the TypeScript website here: https://www.typescriptlang.org/dev/twoslash/
What is Twoslash?
It might be easier to show instead of telling, here is an example of code from the TypeScript handbook. We'll use
twoslash to let the compiler handle error messaging and provide rich highlight info.
Before
Tuple types allow you to express an array with a fixed number of elements whose types are known, but need not be the same. For example, you may want to represent a value as a pair of a string
and a number
:
```ts
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
// Initialize it incorrectly
x = [10, "hello"]; // Error
```
When accessing an element with a known index, the correct type is retrieved:
```ts
console.log(x[0].substring(1)) // OK
console.log(x[1].substring(1)) // Error, 'number' does not have 'substring'
```
After
Tuple types allow you to express an array with a fixed number of elements whose types are known, but need not be the same. For example, you may want to represent a value as a pair of a string
and a number
:
```ts twoslash
// @errors: 2322
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["hello", 10];
// Initialize it incorrectly
x = [10, "hello"];
```
When accessing an element with a known index, the correct type is retrieved:
```ts
// @errors: 2339
let x: [string, number];
x = ["hello", 10]; // OK
/// ---cut---
console.log(x[0].substring(1));
console.log(x[1].substring(1));
```
See it in action on the site.
What Changed?
Switching this code sample to use twoslash has a few upsides:
- The error messages in both are provided by the TypeScript compiler, and so we don't need to write either "OK" or "Error"
- We explicitly mark what errors are expected in the code sample, if it doesn't occur twoslash throws
- The second example is a complete example to the compiler. This makes it available to do identifier lookups and real compiler errors, but the user only sees the last two lines.
On the flip side, it's a bit more verbose because every twoslash example is a unique compiler environment - so you need to include all the dependent code in each example.
Features
The Twoslash markup language helps with:
- Enforcing accurate errors from a TypeScript code sample, and leaving the messaging to the compiler
- Splitting a code sample to hide distracting code
- Declaratively highlighting symbols in your code sample
- Replacing code with the results of transpilation to different files, or ancillary files like .d.ts or .map files
- Handle multi-file imports in a single code sample
- Creating a playground link for the code
Notes
- Lines which have
// prettier-ignore
are stripped
API
The twoslash markup API lives inside your code samples code as comments, which can do special commands. There are the following commands:
export interface ExampleOptions {
noErrors: boolean
errors: number[]
showEmit: boolean
showEmittedFile: string
noStaticSemanticInfo: boolean
emit: boolean
noErrorValidation: boolean
}
In addition to this set, you can use @filename
which allow for exporting between files.
Finally you can set any tsconfig compiler flag using this syntax, which you can see in some of the examples below.
Examples
compiler_errors.ts
function fn(s) {
console.log(s.subtr(3))
}
fn(42)
Turns to:
function fn(s) {
console.log(s.subtr(3))
}
fn(42)
With:
{
"code": "See above",
"extension": "ts",
"highlights": [],
"queries": [],
"staticQuickInfos": "[ 7 items ]",
"errors": [
{
"category": 1,
"code": 7006,
"length": 1,
"start": 13,
"line": 1,
"character": 12,
"renderedMessage": "Parameter 's' implicitly has an 'any' type.",
"id": "err-7006-13-1"
}
],
"playgroundURL": "https://www.typescriptlang.org/play/#code/PTAEAEBcEMCcHMCmkBcoCiBlATABgIwCsAUCBIrLAPawDOaA7LrgGzHEBmArgHYDGkAJZUeoDjwAUtAJSgA3sVCg+I2lQA2iAHTqq8KVtpcARpFgSAzNOnEAvu3ESALNhtA",
"tags": []
}
compiler_flags.ts
function fn(s) {
console.log(s.subtr(3))
}
fn(42)
Turns to:
function fn(s) {
console.log(s.subtr(3))
}
fn(42)
With:
{
"code": "See above",
"extension": "ts",
"highlights": [],
"queries": [],
"staticQuickInfos": "[ 7 items ]",
"errors": [],
"playgroundURL": "https://www.typescriptlang.org/play/#code/PTAEAEDsHsEkFsAOAbAlgY1QFwIKQJ4BcoAZgIbIDOApgFAgRZkBOA5tVsQKIDKATAAYAjAFZa9MABUAFqkqgA7qmTJQMLKCzTm0BaABG1dGQCuNUNBKbp1NXCRpMuArRInI6LKmiRSkABSUAJSgAN60oKDoPpTQyNQAdMjQrIEJlCb6WMz+AMxBQbQAvuIkAQAsfEEA3LRAA",
"tags": []
}
completions.ts
console.log
Turns to:
console.log
With:
{
"code": "See above",
"extension": "ts",
"highlights": [],
"queries": [
{
"completions": [
{
"name": "assert",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "clear",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "count",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "countReset",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "debug",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "dir",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "dirxml",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "error",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "group",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "groupCollapsed",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "groupEnd",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "info",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "log",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "table",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "time",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "timeEnd",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "timeLog",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "timeStamp",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "trace",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
},
{
"name": "warn",
"kind": "method",
"kindModifiers": "declare",
"sortText": "11"
}
],
"kind": "completions",
"start": 9,
"completionsPrefix": "l",
"length": 1,
"offset": 9,
"line": 1
}
],
"staticQuickInfos": "[ 2 items ]",
"errors": [],
"playgroundURL": "https://www.typescriptlang.org/play/#code/MYewdgziA2CmB00QHMBQB6dACHusD0AfVIA",
"tags": []
}
cuts_out_unnecessary_code.ts
interface IdLabel {
id: number
}
interface NameLabel {
name: string
}
type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented"
}
let a = createLabel("typescript")
let b = createLabel(2.8)
let c = createLabel(Math.random() ? "hello" : 42)
Turns to:
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented"
}
let a = createLabel("typescript")
let b = createLabel(2.8)
let c = createLabel(Math.random() ? "hello" : 42)
With:
{
"code": "See above",
"extension": "ts",
"highlights": [],
"queries": [
{
"docs": "",
"kind": "query",
"start": 354,
"length": 16,
"text": "let a: NameLabel",
"offset": 4,
"line": 5
},
{
"docs": "",
"kind": "query",
"start": 390,
"length": 14,
"text": "let b: IdLabel",
"offset": 4,
"line": 7
},
{
"docs": "",
"kind": "query",
"start": 417,
"length": 26,
"text": "let c: NameLabel | IdLabel",
"offset": 4,
"line": 9
}
],
"staticQuickInfos": "[ 14 items ]",
"errors": [],
"playgroundURL": "https://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgJIBMAycBGEA2yA3ssOgFzIgCuAtnlADTID0AVMgM4D2tKMwAuk7I2LZAF8AUKEixEKAHJw+2PIRIgVESpzBRQAc2btk3MAAtoyAUJFjJUsAE8ADku0B5KBgA8AFWQIAA9IEGEqOgZkAB8ufSMAPmQAXmRAkLCImnprAH40LFwCZEplVWL8AG4pFnF-C2ARBF4+cC4Lbmp8dCpzZDxSEAR8anQIdCla8QBaOYRqMDmZqRhqYbBgbhBkBCgIOEg1AgCg0IhwkRzouL0DEENEgAoyb3KddIBKMq8fdADkkQpMgQchLFBuAB3ZAAInWwFornwEDakHQMKk0ikyLAyDgqV2+0OEGO+CeMJc7k4e2ArjAMM+NTqIIAenkpjiBgS9gcjpUngAmAB0AA5GdNWezsRBcQhuUS+eongBZQ4WIVQODhXhPT7IAowqz4fDcGGlZAAFgF4uZyDZUiAA",
"tags": []
}
declarations.ts
export function getStringLength(value: string) {
return value.length
}
Turns to:
export declare function getStringLength(value: string): number
With:
{
"code": "See above",
"extension": "ts",
"highlights": [],
"queries": [],
"staticQuickInfos": "[ 0 items ]",
"errors": [],
"playgroundURL": "https://www.typescriptlang.org/play/#code/PTAEAEBMFMGMBsCGAnRAXAlgewHYC5Q1kBXaAKBAgGcALLAdwFEBbDNCscWhlttaSADEM8aAQw4YADwB0kGWipkKAKhVlQK0AHFoiwjWihROAOZoaoLADNQiUFSITTGreAAOKRM1AA3RPCkdg5OZq7AZNBS7ljIaKDWxDiwmLigpnoAyqGmADLQZhYAFP6BYiHIzgCUoADeGqDIesTIOH4BpDIm5jRkAL5kQA",
"tags": []
}
errorsWithGenerics.ts
const a: Record<string, string> = {}
let b: Record<string, number> = {}
b = a
Turns to:
const a: Record<string, string> = {}
let b: Record<string, number> = {}
b = a
With:
{
"code": "See above",
"extension": "ts",
"highlights": [],
"queries": [],
"staticQuickInfos": "[ 6 items ]",
"errors": [
{
"category": 1,
"code": 2322,
"length": 1,
"start": 72,
"line": 2,
"character": 0,
"renderedMessage": "Type 'Record<string, string>' is not assignable to type 'Record<string, number>'.\n 'string' index signatures are incompatible.\n Type 'string' is not assignable to type 'number'.",
"id": "err-2322-72-1"
}
],
"playgroundURL": "https://www.typescriptlang.org/play/#code/PTAEAEFMCdoe2gZwFygEwGY1oFAGM4A7RAF1AENUAlSA6AEwB5ToBLQgcwBpQX2OAfKAC8oAN4BfHABtIZAEbVaCJn049CAVwC28mENGSc8kRRxA",
"tags": []
}
highlighting.ts
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`)
}
greet("Maddison", new Date())
Turns to:
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`)
}
greet("Maddison", new Date())
With:
{
"code": "See above",
"extension": "ts",
"highlights": [
{
"kind": "highlight",
"offset": 134,
"length": 10,
"text": "",
"line": 4,
"start": 18
}
],
"queries": [],
"staticQuickInfos": "[ 11 items ]",
"errors": [],
"playgroundURL": "https://www.typescriptlang.org/play/#code/GYVwdgxgLglg9mABAcwE4FN1QBQAd2oDOCAXIoVKjGMgDSIAmAhlOmQCIvoCUiA3gChEiCAmIAbdADpxcZNgAGACXTjZiACR98RBAF96UOMwCeiGIU19mrKUc6sAypWrzuegIQLuAbgF6BATRMHAAiAFkmBgYLBFD6MHQAd0QHdGxuXwEAemzhfILC4QA9UrLygSA",
"tags": []
}
import_files.ts
export const helloWorld = "Example string"
import { helloWorld } from "./file-with-export"
console.log(helloWorld)
Turns to:
export const helloWorld = "Example string"
import { helloWorld } from "./file-with-export"
console.log(helloWorld)
With:
{
"code": "See above",
"extension": "ts",
"highlights": [],
"queries": [],
"staticQuickInfos": "[ 5 items ]",
"errors": [],
"playgroundURL": "https://www.typescriptlang.org/play/#code/PTAEAEDMEsBsFMB2BDAtvAXKGCC0B3aAFwAtd4APABwHsAnIgOiIGcAoS2h0AYxsRZFQJeLFg0A6vVgATUAF5QAIgCiFNFQShBdaIgDmSgNxs2ICDiRpMoPTMrN20VFyEBvEWMnSZAX2x0NKjKjMCWBMRknPRESmx8AjQIjOL6ABSe4lJ0sgCUbEA",
"tags": []
}
importsModules.ts
import React from "react"
export function Hello() {
return (
<div>
<h1>Hello World</h1>
</div>
)
}
import { Hello } from "./Component"
console.log(Hello)
Turns to:
import React from "react"
export function Hello() {
return (
<div>
<h1>Hello World</h1>
</div>
)
}
import { Hello } from "./Component"
console.log(Hello)
With:
{
"code": "See above",
"extension": "ts",
"highlights": [],
"queries": [],
"staticQuickInfos": "[ 10 items ]",
"errors": [],
"playgroundURL": "https://www.typescriptlang.org/play/#code/PTAEAEDMEsBsFMB2BDAtvAXKAwge1QA66JIAuAdKQM4AeAUNIbgE6mgBK8yAxm5M-lAAiZl15C6deDSKtQkAK6Je0YqAAS8WLFwAKAJSgA3nVChRpBc0Shdps6AA8AE2gA3AHz2HTgBYBGD01tXFAAdRZYZ0dgAK8fGNdPe306AF9JEAgYBBR0LGhEZ2lKKgYmOSMNLR1QNPkBVGFyYDwmEkRSCW5iKlwEch0Ac11gnVSgA",
"tags": []
}
query.ts
let foo = "hello there!"
Turns to:
let foo = "hello there!"
With:
{
"code": "See above",
"extension": "ts",
"highlights": [],
"queries": [
{
"docs": "",
"kind": "query",
"start": 4,
"length": 15,
"text": "let foo: string",
"offset": 4,
"line": 1
}
],
"staticQuickInfos": "[ 1 items ]",
"errors": [],
"playgroundURL": "https://www.typescriptlang.org/play/#code/DYUwLgBAZg9jEF4ICIAWJjHmdAnEAhMgNwBQA9ORBAHoD8pQA",
"tags": []
}
showEmit.ts
export function fn(arr: number[]) {
const arr2 = [1, ...arr]
}
Turns to:
var __read =
(this && this.__read) ||
function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator]
if (!m) return o
var i = m.call(o),
r,
ar = [],
e
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value)
} catch (error) {
e = { error: error }
} finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i)
} finally {
if (e) throw e.error
}
}
return ar
}
var __spreadArray =
(this && this.__spreadArray) ||
function (to, from, pack) {
if (pack || arguments.length === 2)
for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i)
ar[i] = from[i]
}
}
return to.concat(ar || Array.prototype.slice.call(from))
}
export function fn(arr) {
var arr2 = __spreadArray([1], __read(arr), false)
}
With:
{
"code": "See above",
"extension": "js",
"highlights": [],
"queries": [],
"staticQuickInfos": "[ 0 items ]",
"errors": [],
"playgroundURL": "https://www.typescriptlang.org/play/#code/PTAEAEGcAsHsHcCiBbAlgFwFAgughgE4DmApugFyiIDKArNmOACYIB2ANiQG4nsYkE86VLFaYGoALSTUyAA6wC6ABK85AyKFGVqcgiTxNQ0NQNDxU7dqABGJULIVKSRgGYFYyUAHJ0kPjbe4iQAHk7ooK4ArqwAxsKikawAFIQElKxRyHYEANoAugCUoADemKCgsaKQEWkATKAAvKC5AIwANKAAdD1p+ZgAvphAA",
"tags": []
}
API
The API is one main exported function:
export function twoslasher(code: string, extension: string, options: TwoSlashOptions = {}): TwoSlashReturn
Which takes the options:
export interface TwoSlashOptions {
defaultOptions?: Partial<ExampleOptions>
defaultCompilerOptions?: CompilerOptions
customTransformers?: CustomTransformers
tsModule?: TS
tsLibDirectory?: string
lzstringModule?: LZ
fsMap?: Map<string, string>
vfsRoot?: string
customTags?: string[]
}
And returns:
export interface TwoSlashReturn {
code: string
extension: string
highlights: {
kind: "highlight"
start: number
line: number
offset: number
text?: string
length: number
}[]
staticQuickInfos: {
targetString: string
text: string
docs: string | undefined
start: number
length: number
line: number
character: number
}[]
queries: {
kind: "query" | "completions"
line: number
offset: number
text?: string
docs?: string | undefined
start: number
length: number
completions?: import("typescript").CompletionEntry[]
completionsPrefix?: string
}[]
tags: {
name: string
line: number
annotation?: string
}[]
errors: {
renderedMessage: string
id: string
category: 0 | 1 | 2 | 3
code: number
start: number | undefined
length: number | undefined
line: number | undefined
character: number | undefined
}[]
playgroundURL: string
}
Using this Dependency
This package can be used as a commonjs import, an esmodule and directly via a script tag which edits the global namespace. All of these files are embedded inside the released packages.
Local Development
Below is a list of commands you will probably find useful. You can get debug logs by running with the env var of DEBUG="*"
.
npm start
or yarn start
Runs the project in development/watch mode. Your project will be rebuilt upon changes. The library will be rebuilt if you make edits.
npm run build
or yarn build
Bundles the package to the dist
folder. The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module).
npm test
or yarn test
Runs the test watcher (Jest) in an interactive mode. By default, runs tests related to files changed since the last commit.