-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature Request] Rust-Like Macros that generate TypeScript (again) #60645
Comments
Duplicate of #4892. IMO opening a duplicate of an issue just because you don't like the decision is really not the way to good, especially when the definitive decision is only a year old. |
It's slightly different to the original issue as it elaborates more on the concept and addresses some of the concerns of the original. Though I do understand the sentiment and apologise if it's inappropriate |
We were aware of everything said in this issue the previous times we've decided not to do macros. I don't see anything new here. The scope of this project is very well-defined and unfortunately I don't know how to articulate it any more clearly than we already have. |
Fair enough, I'll close the ticket. Part of the desire to include this into the TypeScript core is the lack of alternative pathways to add a feature like this. I realize this opens the door to substantial fragmentation, but has the team considered enabling plugins for the compiler & LSP to facilitate the experimentation of these sorts of concepts? I was thinking I could implement callable macros as jsdoc style comments as that's easy enough to work with in the existing compiler. However without LSP support that would be DOA. function App() {
const value = "World"
return /** @macro jsx!(<div>Hello{value}</div>) */
}
/** @macro #[struct] */
class Foo {
bar: string
} |
π Search Terms
macros, macro, rust
β Viability Checklist
β Suggestion
I know this is an idea that has been beaten to death but I wanted to present a fresh case and slightly different approach for it as I do believe that there is substantial value we are missing out on.
TypeScript already has a macro; built-in support to convert JSX.
The suggestion being presented here is to generalize that idea to allow for new use cases and expand the ability for framework developers to utilize TypeScript without the need to create external compilers and LSPs/editor plugins (
.vue
.svelte
, Angular'sngc
).Rust provides a well tested blueprint for a macro system that, on the surface, appears to fit well with TypeScript's design goals. That is; rather than expanding directly to JavaScript - macros expand to TypeScript which then is available to the type checker and LSP.
The simplest illustration of this is jsx. Jsx is a built-in macro included in TypeScript.
While not practical from a backwards compatibility standpoint, as an example, it would be possible to implement jsx as a callable macro.
From
To
I'm not fussed about the specifics of the syntax, just the functionality. In this proposal I will borrow Rust's syntax for illustrative purposes.
The use of the
!
injsx!()
indicates that is it evaluated at compile time and expands into valid TypeScript.The advantage of this is that the implementation of the macro would be externally maintained outside of the TypeScript core by the teams that utilize it (e.g. the React team).
Configuration (e.g.
jsxFactory
) would be targeted at the macro library rather than TypeScript'scompilerOptions
. This simplifies the objectives and configuration of TypeScript.But why?
In the front end world, we have many frameworks - but the only framework (forgive me for calling it that) that is practically supported by TypeScript is React and its derivatives. This is due to built-in support for expanding jsx syntax to its equivalent JavaScript syntax.
Other frameworks, like Angular, Vue, Svelte must all implement their own compilers, custom file formats and editor plugins (basically LSPs) to operate.
This increases the difficulty of innovating in the FE framework landscape, causing fragmentation, increasing the tooling burden for end users and preventing tool makers the ability to allow end users to update TypeScript beyond the version supported by their bespoke compiler (as is the case with Angular).
Some frameworks, like Vue and Svelte, are forced to use custom file formats, this causes difficulty when integrating with existing tooling (linters, formatters, test runners, nodejs).
How do macros solve these issue?
Formalizing compile time macros allows framework maintainers to use TypeScript directly rather than by using external bespoke compilers and stitching them together with editor/bundler plugins.
In addition, macros would expand into valid TypeScript at compile time, giving end users LSP warnings (which isn't possible with decorators) and type checking on the expanded syntax.
We see evidence of the success of this model in the Rust world; where there are many examples of GUI frameworks using no additional tooling and providing vastly different syntactic approaches to expressing GUI structures. Some resemble React, others resemble Vue.
FE Framework Examples
Compile time macros would allow a framework like Vue to use a macro for template compilation in its component declarations. This would remove the need for
.vue
files and the tooling associated to handle them.Angular could do the same
While the projects would still need bespoke compilers to parse/transform the contents of the code within their macros - that tooling would not be external and thus it would be natively supported by common tools (linters, formatters, test runners, etc).
It would also open the door to new types of frameworks - for instance Mobx-style observability could be more succinctly expressed with attribute macros. Something that currently requires painful amounts of runtime code and monkey patching.
Different types of macros
Take these examples with a grain of salt, I'm not the most creative person so these are more designed to illustrate how they work rather than examples of good macros that people would use.
Note: Only the macro engine is built into TypeScript, the macros themselves are implemented as user-land libraries that are installed via package manager or hand written
Callable macro
A callable macro is a function-like expression that accepts an arbitrary argument which it parses and returns valid TypeScript from at compile time.
Example of a macro that adds two numbers at compile time:
Expands to:
Then transpiles to:
Note that the argument passed in is arbitrary.
A conventional sum function would take
sum(4, 5)
as two arguments but the macro takessum!(4 + 5)
.Behind the scenes, TypeScript would pass the internals of the macro call (you can think of everything within the
sum!(<-->)
as being supplied to the macro as a string) to the implementation, which then parses the input and computes the operation at compile time, replacing itself with valid TypeScript.This means you can have a macro that accept jsx, html, csv, xml or some kind of custom language. It's language-ception.
Procedural macro
A procedural macro is an attribute for variables, classes and properties that augment the target, much like a decorator but at compile time.
Example of a macro that offers an "initializer" for a class and zero-values (making it struct-like):
Expanding to something like
Other Examples
Outside of the frontend framework world, many operations are needlessly done on the client. Having access to an API that allows for compile time computation means we have the potential to trade some loss in compile time for a speed up in runtime performance.
Macros could be written to embed external content into a file:
A macro could be written to embed environment variables into the output
There are also use cases for macros where decorators were previously considered but ultimately unsuitable - like serializing/deserializing protobuf/arrow/etc and expressing database schemas as JavaScript objects and so on.
For brevity I will leave them out
Cons
Compile times may take a hit in projects that make heavy use of macros. This can be mitigated by leveraging wasm or napi modules within the macro implementations.
The general idea is that the complexity is handled by the macro maintainers while the end user has a simple api to work with.
Macros VS TypeScript's Design Goals
Examining how this fits into TypeScripts design goals:
Implementation
Given the historically controversial nature of this topic, for now I will just say "look at Rust macros as a reference".
Comment Based
If TypeScript provided the ability to extend the LSP, it may be possible to implement this outside of the core as a preprocessor to TypeScript.
Mainline
If there is traction to incorporating this, I would be happy to expand on this and work on a reference implementation as long as there is a approval/desire to incorporate it into mainline TypeScript.
Forking TypeScript and producing a derivative that incorporates these features is undesirable as I feel it would be largely wasted effort.
The text was updated successfully, but these errors were encountered: