TypeScript
TypeScript copied to clipboard
Feature Request: Macros
This one might be waaaaaay out of scope, but I think it is worth proposing. The idea here is to add support for macros, functions that run at compile time, taking one or more AST nodes and returning an AST node or array of AST nodes. Examples/use cases: (Syntax off the top of my head, open to better ideas)
Interface-Based Validation
// validation.macros.ts
function interfaceToValidatorCreator(interface: ts.InterfaceDeclaration): ts.Statement {
// Would return something like "const [interface.name]Validator = new Validator({...});",
// using types from the interface
}
macro CreateValidator(interface: ts.InterfaceDeclaration) {
return [interface, interfaceToValidator(interface)];
}
// mainFile.ts
/// <macros path='./valididation.macros.ts' />
@#CreateValidator // Syntax 1: Decorator-Style
interface Person {
name: string;
age: number;
}
PersonValidator.validate(foo)
Type Providers
// swagger.macros.ts
macro GetSwaggerClient(url: ts.StringLiteral): AssertionExpression {
// return something like "new SwaggerClient([url]) as SwaggerClientBase & {...}" where
// ... is an object creating the methods generated from the URL.
}
// mainFile.ts
/// <macros path='./swagger.macros.ts' />
var fooClient = #GetSwaggerClient("http://foo.com/swagger.json"); // Syntax 2: Function call syntax
fooClient.getPeople((people) => {
people.every((person) => console.log(person.firstName + "," + person.lastName);
});
Conditional Compilation
// conditional-compilation.macros.ts
macro IfFlagSet(flagName: ts.StringLiteral, code: ts.Statement[]): ts.Statement[] {
return process.env[flagName.string] ? code : []
}
// mainFile.ts
/// <macros path='./compilation.macros.ts' />
#IfFlagSet("DEVELOPMENT") { // Syntax 3: Language Construct-Like (multiple arguments can be passed in parentheses)
expensiveUnnecessarySanityCheck()
}
Notes
- Macros would run right after parsing. Not sure how we would deal with macros that need type information.
- This would make running
tsc
on unknown code as dangerous as running unknown code. It might be good to require a--unsafeAllowMacros
argument, not settable from atsconfig.json
. - It might be worth nothing in the docs that the AST format may change at any time, or something along those likes
- The
macro
keyword would probably compile to a function, followed by ats.registerMacro(function, argumentTypes, returnType
call. - Macros must be typed as returning a AST interface. This means that functions creating ASTs will probably need to have an explicit return type (or a calling function could have an explicit return type.
- Alternatively, we could consider giving the
kind
property special treatment inmacros.ts
files.
- Alternatively, we could consider giving the
- Just because a proposed syntax looks like a normal typescript construct doesn't mean it behaves like one.
#Foo(interface Bar{})
is valid syntax, as long as there is a macro namedFoo
that takes an interface.- Exception: The Decorator syntax might need to be a bit more choosy (no decorating
1 + 1
, but decorating Interfaces, interface items, functions, etc. should be fine.
- Exception: The Decorator syntax might need to be a bit more choosy (no decorating
- This issue is likely to be updated quite a bit. For a log of changes, see the gist
duplicate of #3136?
@mhegazy I don't think so. Support for macros as AST->AST functions would let us do anything you could do with a type provider (just return an AssertionExpression
, see the second example in the issue text), but also conditional compilation (return the passed code if the condition is true, otherwise do nothing), as well as general boilerplate reduction.
+1
+1
+1 (I almost expected it to work with sweet.js.)
+1
+1
You can also look at the haxe macro system how they implemented it. http://haxe.org/manual/macro.html
+1
+1 :+1:
Question Could this be provided by a pre-compile hook; between slurping the .ts file and depositing the .js file?
Possible Benefits
- Allows for competing macro processors, eventually yielding a best-in-class ? or 500 of them...
- Natural requirement is production of valid TypeScript; but that's now on the user
- Allowed to evolve separately and distinctly along a rigorous path for macro definitions and implementations (ala regular expressions)
Of course, I could be missing some fundamental concept of macros and compilers, rather than just attempting to break up the traditional view.
+1
+1
I need this. My needed use-case right now would be to make sort-of an "inline-function", or similar to a C-preprocessor-like parametric macro. That code would be inlined to avoid the function call on the JavaScript output.
C Macro Example:
#define ADD(x, y) ((x) + (y))
C inline Example:
inline int ADD(int x, int y) {
return x + y;
}
I'd write something similar in TypeScript (let's assume the keyword inline will work only with functions that can be inlined).
In TypeScript the inline
approach would look like this:
inline function ADD(x: number, y: number) {
return x + y;
}
UPDATE: Looks like a similar issue was already here #661
I would enjoy some form of macros for sure. As great as typescript is, it's still crappy old JS underneath. Something I would want to macro in would be proper if/else expressions.
👍
migrated from #11536 (which was closed in favor of this one)
situation:
-
sometimes i wish i could generate a piece of code based on some existing one, for example a construction function for an given interface
interface MyConfig { name: string; values: number[]; } function myConfigFrom(name: string, values: number[]) : MyConfig { return { name, values }; }
problem: currently my options are
- either write it by hands (tedious monotonous work)
- put together a homemade code generator and run it as a pre-build step (lot of maintenance, non-standard)
solution:
-
allow AST rewrites via decorators
@rewrite(addContructorFunction) interface MyConfig { name: string; values: number[]; } function addContructorFunction(node: ts.Node): ts.Node[] { return [node, toConstructorFunction(node as ts.InterfaceDeclaration)]; } function toConstructorFunction(node: ts.IntefaceDeclaration): ts.FunctionDeclaration { // fun stuff goes here }
This would be huge. In something like Scala, macros are a way for the community to implement and test out new language features that are not yet (or will never be) supported by the core language.
After adding macro support, TS would have a large laboratory of potential features to draw on when implementing new ones, and could gauge support and feasibility of a feature before implementing it.
Features like pattern matching could first be implemented as macros, and then either moved into a standard macro lib, or into TS core if they are broadly useful and popular. This takes a burden off TS maintainers and authors, and gives the community freedom to experiment without forking the TS compiler.
FWIW, I think that a more promising direction is for a macro facility to accommodate TS. The obvious example would be to extend sweet.js so it accepts the TS syntax, and expands into TS code. This way, TS doesn't need to know about macros at all.
This leads to something very similar to Typed Racket (for anyone who knows that), including the minor disadvantage of not being able to write macros that depend on types.
@elibarzilay With that approach, would macros be typesafe? If the whole point of TS is to be a typesafe layer on top of JS, macros should ideally also be typesafe.
Again comparing to Scala macros, their safety is a huge win. Otherwise you end up shooting in the dark without IDE/compiler support until you get something that compiles.
@bcherny: The macro code itself wouldn't be typed. But that's minor IMO (since at that level it's all ASTs in and out). (Compared to random scala macros that I've seen after a few seconds of grepping the web, you get only Expr
with no type qualification.)
The code that macros produce might not be well typed, but it still goes through the type checker which does verify that the result is safe.
I think this is something similar to c/c++ perprocessor maybe, with type check? But i really want to write something like that:
#IfFlagSet("DEVELOPMENT") {
macro assert(cond: any, message?: string) {
if (!cond) { throw new Error("...") }
}
} else {
macro assert(...x: any[]) // or something similar, and in this case dont emit code for this macro call
}
(Similar, but a proper macro system compared to CPP is like comparing JS to machine code...)
+1
Hygienic macro. https://en.wikipedia.org/wiki/Hygienic_macro
JS is a mixed bag. It has some nice features, and some really awful ones. Also, new features take a very long time to get voted, approved by TC39 then implemented by the browsers. Political agendas may sometimes block some great features.
Macros could help us implement some very useful things in user land. I would love to use this right now: https://github.com/mindeavor/es-pipeline-operator
+1
My example is one of having interfaces (that also use extends) that describe websocket network messages with binary specific types (type int8 = number
), and wanting to generate code for something like https://github.com/phretaddin/schemapack
Interestingly I've done exactly this without macros and it was a pain in the rear both for me and future developers to the project. But it also saved an incredible amount of time considering there were over 100 different network message interfaces.
Another use case: Given a simple or discriminated union, generate the list of all the possible values. This can help with mistakes where things are modified in one place but not the other.
+1
+1