Modes #
typestrip has two modes. Pick one with the mode option. Erase mode is the default.
await transform(src); // erase mode
await transform(src, { mode: "downlevel" });
Erase mode ("erase") |
Downlevel mode ("downlevel") |
|
|---|---|---|
Erasable syntax (types, annotations, interface, type, declare, generics, as, satisfies, !, and so on) |
Erased | Erased |
| Non-erasable syntax | Reported | Down-leveled |
| Positions | Every offset preserved | Every line preserved; columns preserved outside converted constructs |
| Engine | build/erase.wasm |
build/downlevel.wasm |
| Oracle | ts-blank-space | ts.transpileModule |
Neither mode type-checks, and neither lowers JavaScript to an older target. An input that TypeScript's parser accepts but its checker rejects (two extends clauses, for example) is stripped as parsed.
Erase mode #
Erase mode erases erasable syntax, the same way ts-blank-space and Node's type stripping do. The output needs no source map.
Position guarantee. The output has the same UTF-16 length as the input, and every kept character is at its original offset. Each erased character becomes a space. Line terminators are kept. A few erased characters become ;, ( or ) where that keeps ASI and arrow-function semantics.
let x: number = 1;
let x = 1;
Non-erasable syntax is reported, not changed. Each of these constructs is an UnsupportedSyntaxError, or an onError call when you pass onError:
enumandconst enum- an instantiated
namespaceormodule - parameter properties (
constructor(private x: number)) import x = require("y")andimport x = A.Bexport =- legacy
<T>xcasts
Downlevel mode #
Downlevel mode erases everything erase mode erases. It also down-levels the non-erasable constructs listed above, matching what ts.transpileModule does at runtime. Every other piece of the file is identical to erase mode's output.
Position guarantee. Every line is preserved. A converted construct is rewritten on its own lines, so columns inside it, and after it on the same line, can shift. Columns everywhere else are preserved. Inserted text never contains a line break.
Output shape #
An enum or instantiated namespace becomes a function called with the container object as this. The source's own braces are the function body.
enum E { A, B = A + 2 }
var E;(function(E) { A=0;var A;this.A=A;this[A]="A"; B = A + 2;var B;this.B=B;typeof B=="string"||(this[B]="B"); }).call(E||(E={}),E);
namespace N { export const a = 1; }
var N;(function(N) { const a = 1;;this.a=a; }).call(N||(N={}),N);
A parameter property becomes a field and an assignment at the top of the constructor body.
class P { constructor(private x: number) {} }
class P {x; constructor( x ) {this.x=x;} }
import x = require("y") becomes const x = require("y"), import x = A.B becomes var x = A.B, and export = x becomes module.exports = x.
What downlevel mode still reports #
- A legacy cast where erasing it would trigger ASI. Downlevel mode erases
<T>xcasts, except when the cast is the operand ofreturn,throw, or a non-starredyield, and a line terminator sits between the cast and its operand. Erasing the cast there would end the statement at the keyword. Downlevel mode reports that one case, the same way erase mode does. - Mixed module styles.
import x = require()andexport =down-level torequireandmodule.exports. A file that also has a value-level ESMimportorexportcan't run under either module system after that. Downlevel mode reports each CommonJS-only construct in such a file and leaves it in place.
JSX #
Pass jsx: true for .tsx input. It works in both modes. It's off by default, because JSX changes how < parses.
await transform(src, { jsx: true }); // erase mode, JSX kept
await transform(src, { jsx: true, mode: "downlevel" }); // JSX written for the classic runtime
With jsx on, typestrip uses TypeScript's .tsx grammar. < at the start of an expression begins a JSX element, so a legacy <T>x cast is JSX, not a cast. A generic arrow function needs a form TypeScript accepts in .tsx: <T,>(x: T) => x, <T extends U>(x: T) => x, or <T = U>(x: T) => x, each optionally with const before T.
Erase mode #
Erase mode keeps the JSX and erases types inside it, and every offset is still preserved. It also blanks type arguments on a tag. ts-blank-space keeps them. No JSX tool accepts <Select<Option> />, but every JSX tool accepts the blanked <Select />.
const el = <Select<Option> value={v as Option} />;
const el = <Select value={v } />;
Downlevel mode #
Downlevel mode writes JSX for the classic runtime. It matches ts.transpileModule with jsx: "react". An element becomes factory(tag, props, ...children), and a fragment becomes factory(fragmentFactory, null, ...children). Every line is preserved. Columns are not preserved inside JSX.
export const a = <div className="box" {...rest}>Hi, {name}!</div>;
export const a = React.createElement("div" ,{className:"box" ,...(rest)},"Hi, ",(name),"!") ;
The output follows TypeScript's rules:
- Tags. A tag whose name starts with a lowercase letter or contains
-is a string ("div"). So is a namespaced tag ("svg:rect"). Any other tag is an expression (Foo,ui.Button,this). - Props. With no attributes, the props argument is
null. An attribute with no value istrue. A spread attribute stays a spread. A name with-or:becomes a string key. - Text. Each line of a text child is trimmed, lines with only whitespace are dropped, and the remaining lines are joined with one space. Whitespace at the start of the first line and at the end of the last line is kept. A text child that spans lines is written as one string per line, joined with
+, so every line stays in place. - Strings. HTML entities in text and attribute strings are decoded (
&,{,{). An unknown entity stays as written. - Containers.
{expr}becomes(expr), so a comma expression stays one argument.{...items}becomes a spread argument. An empty container, or one holding only a comment, adds no child.
Factories #
| Option | Default | TypeScript equivalent |
|---|---|---|
pragma |
"React.createElement" |
jsxFactory |
pragmaFrag |
"React.Fragment" |
jsxFragmentFactory |
Each option must be an identifier or a dotted name. Any other value throws a TypeError.
A file can set its own factories with comment pragmas. They override the options:
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
TypeScript's pragma rules apply. Only block comments before the first token of the file count. The first @jsx and the first @jsxFrag win. The factory is the first word after the pragma name. If that word is not an identifier or a dotted name, the option applies.
Downlevel mode does not support the automatic runtime (react/jsx-runtime). It ignores @jsxRuntime and @jsxImportSource pragmas.
Choosing a mode #
Use erase mode when the code already runs under Node's type stripping, or under any tool with the same rules (erasableSyntaxOnly in tsconfig.json). It is smaller, and every position matches the source.
Use downlevel mode when the code uses enums, namespaces, or parameter properties and you can't change it. Line numbers still match the source, so stack traces and DevTools line positions work without a source map.
The two engines are separate binaries. A program that only uses downlevel mode never loads erase.wasm, and the reverse is also true. See size in the README.