Native TypeScript Type Stripping in Boa's Parser #4707
Replies: 4 comments 7 replies
-
Source: https://github.com/tc39/proposal-type-annotations The tc39 proposal to add type annotations to the javascript language itself also provides additional justifications as to why supporting type annotations in the language is a good idea. |
Beta Was this translation helpful? Give feedback.
-
|
A counterargument of this proposal is that for most folks, having something that "kind" of compiles Typescript but not really is worse than just straight up telling everyone that we do not support Typescript. Imagine you're an embedder and you happily pass your Typescript files to Boa, and they compile and run perfectly fine. Everything's good and you deploy to prod knowing that you didn't need to create a whole Typescript pipeline for it. Then, 3 months down the line, you slightly change your Typescript code and now everything breaks because Boa didn't support the Typescript feature you used, and now you require bundling a Typescript compiler. This would be very annoying for users that expect Boa to "just work". That's why right now I'm very wary about adding Typescript support. because it's either we don't support it and delegate that to Typescript compilers, or we do and we need to properly support consts, enums, namespace, etc. Also, this is kind of null if https://github.com/tc39/proposal-type-annotations goes forward, so my preference would be to help push that proposal onto stage 2 and implement that when the time comes. |
Beta Was this translation helpful? Give feedback.
-
|
Also, Deno, Node and Bun are Runtimes, not engines, so they have the liberty of just bundling a Typescript compiler, because they can be opinionated about what users to serve and what use cases to enable. We don't have that liberty unfortunately. |
Beta Was this translation helpful? Give feedback.
-
|
What would be feasible though is to have some kind of API for converting a Typescript AST compiled by oxc/swc into our own AST representation, preserving all source code information in the way. This way users can just "hook" their favorite TS compiler in the compilation pipeline, then pass that transparently to Boa for Bytecode compilation. |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
I want to propose modifying Boa's parser to natively strip TypeScript type annotations at parse time, so
.tsfiles can be executed directly without any prior transpilation step. This is explicitly not about implementing a type checker, a TypeScript compiler, or anything like that — purely discarding type-level syntax during parsing, leaving all JavaScript semantics untouched.I also want to propose a clear policy: any TypeScript feature that introduces runtime semantics —
const enum,namespace, legacy decorators, etc. — should be a hard parse error. We are not implementing TypeScript. We are implementing type stripping.Motivation
TypeScript is just reality now
Most JavaScript written today is TypeScript. Requiring users to run a transpilation step before handing code to Boa makes us a second-class runtime for the majority of real-world JS projects before we even get evaluated. Type stripping is the minimum viable thing we can do to fix that.
The maintenance concern is overstated
I've seen the objection that supporting TypeScript means tracking TypeScript's release cadence on top of ECMA262 and ECMA402. I think this conflates type stripping with full TypeScript support — they aren't the same thing at all.
TypeScript's type annotation syntax sits in well-defined, stable grammatical positions that have no effect on control flow or semantics:
: Typeafter parameters and variable bindings<T>generic annotations on functions and callsas Typecast expressionsinterfaceandtypealias declarationspublic,private,protected,readonly) on class members!non-null assertionssatisfies TypeexpressionsThese positions have been stable for years. They basically can't change without breaking the entire TypeScript ecosystem. The maintenance surface is small, well-bounded, and completely different in character from tracking evolving ECMA spec semantics. If TypeScript adds a new annotation position in a future release, that's a contained, small fix — not a systemic commitment.
Node.js v22 shipped native type stripping and made the same judgment call. Deno has had it since v1.0. Bun has it. These are production runtimes with far higher stability requirements than us, and they all concluded the tradeoff was worth it.
Transpilation pipelines are genuinely painful
Every transformation step between source and execution introduces real problems that I think we underestimate:
Debugging is broken. Transpiled and bundled output produces stack traces like:
Source maps are supposed to fix this but they drift out of sync, don't survive all transform pipelines cleanly, and add their own complexity. With native type stripping the stripped output is essentially identical to the original source — line numbers, column numbers, variable names all 1:1. Stack traces just point at your actual code.
Transpilers introduce their own bugs. Targeting older JS versions means nullish coalescing gets rewritten, async/await gets transformed, class fields get emulated — all with subtle behavioral differences from native semantics. Type stripping touches nothing semantic. The JS that executes is exactly what you wrote.
Build environments are non-deterministic. The same TypeScript source can produce meaningfully different JavaScript depending on
tsconfig.jsonsettings, which transpiler you're using (tsc,esbuild,swc,babel), and what version of each. Native type stripping is deterministic and has zero configuration surface.This is especially important for Boa's embedded use case
Boa's pitch is being an embeddable Rust-native JS engine. If someone embeds Boa to offer scripting in their Rust application, their users are going to want to write TypeScript. Without type stripping, the host application has to ship a transpiler dependency, manage a build pipeline, and pay a cold-start cost on every script load. Native stripping reduces this to a parse-time token discard — essentially free.
We Don't Have to Implement This From Scratch
This is actually one of the strongest arguments for doing this. There is already mature, production-grade Rust tooling that does exactly this:
--experimental-strip-typesis backed byamarowhich wrapsoxc-transform.We have options. We can integrate one of these crates directly, or we can base our own implementation off their grammar rules without reinventing the wheel. Either way, we're not starting from zero.
Scope
What this covers
interfaceandtypealias declarations (discarded entirely — no runtime artifact)<T>on functions and callsas Typecast expressionspublic,private,protected,readonly)!satisfies Typeexpressionsdeclarestatements (discarded entirely)import typemodule importsimport { type Foo, X }(type Foo) import fields.What this explicitly bans with a hard parse error
Any TypeScript syntax that introduces runtime semantics that can't be expressed as pure JS is a parse error. We are not in the business of compiling TypeScript. If it needs the TypeScript compiler to lower it to JS, we reject it:
const enum— gets inlined by the TypeScript compiler, has no JS equivalentenum— compiles down to an IIFE, introduces real runtime behaviornamespace/module— same problem, compiles to real JS object structureexperimentalDecoratorsbehavior — out of scope entirely, use TC39 decoratorsemitDecoratorMetadata— requires type information at runtime, not our problemThe error message should be clear: "This TypeScript feature introduces runtime semantics and cannot be handled by type stripping. Please compile with tsc first."
Out of scope entirely
.d.ts) generation — not our jobtsconfig.jsonhandling — not our jobImplementation Approach
Type stripping is purely syntactic — no semantic analysis required. The parser hits a token that starts a type annotation, consumes tokens until the annotation ends, discards them, and moves on.
Rough touch points in
boa_parser:Lexer — a small set of new token kinds to recognize type-level syntax.
Colonalready exists but needs context to distinguish type annotation from ternary/object literal. Type keywords to add:type,interface,as,readonly,declare,satisfies,abstract.Parser — annotation skip sites — type annotations appear at predictable grammatical positions. At each site the parser optionally consumes a type annotation and discards it:
Top-level declarations to discard entirely:
Given that swc and oxc are both Rust crates, we should seriously evaluate whether integrating one of them for the stripping pass is lower cost than implementing the grammar rules ourselves. Even if we eventually want our own implementation, using swc/oxc as a reference for the exact grammar positions TypeScript uses is going to save time.
Prior Art
--experimental-strip-typesbacked byamaro/oxc-transformswcunder the hoodOpen Questions
.tsfile extension detection?Conclusion
Type stripping is a small, well-bounded parser feature with a large real-world payoff. It doesn't require us to become a TypeScript compiler, track TypeScript's type system, or take on significant ongoing maintenance. We have mature Rust crates we can lean on directly. The policy of hard-erroring on anything that introduces runtime semantics keeps the scope locked and the implementation honest.
The question isn't whether we want to support TypeScript. The question is whether we want Boa to be usable for the majority of JavaScript code written today.
Beta Was this translation helpful? Give feedback.
All reactions