Proposal: Conditional Compilation
See original GitHub issueProposal: Conditional Compilation
Problem Statement
At design time, developers often find that they need to deal with certain scenarios to make their code ubiquitous and runs in every environment and under every runtime condition. At build time however, they want to emit code that is more suited for the runtime environment that they are targetting by not emitting code that is relevant to that environment.
This is directly related to #449 but it also covers some other issues in a similar problem space.
Similar Functionality
There are several other examples of apporaches to solving this problem:
- In C#, this is solved via conditional flags as well well as conditional symbols.
- In Dojo, this was solved via adopting has.js and static flags in the build tool that would allow build time “dead code removal”.
- IE used to support conditional compilation using comments.
- UglifyJS accomplishes this via assertion of constants coupled with dead code removal.
Considerations
Most of the solutions above use “magic” language features that significantly affect the AST of the code. One of the benefits of the has.js approach is that the code is transparent for runtime feature detection and build time optimisation. For example, the following would be how design time would work:
has.add('host-node', (typeof process == "object" && process.versions && process.versions.node && process.versions.v8));
if (has('host-node')) {
/* do something node */
}
else {
/* do something non-node */
}
If you then wanted to do a build that targeted NodeJS, then you would simply assert to the build tool (staticHasFlags
) that instead of detecting that feature at runtime, host-node
was in fact true
. The build tool would then realise that the else
branch was unreachable and remove that branch from the built code.
Because the solution sits entirely within the language syntax without any sort of “magical” directives or syntax, it does not take a lot of knowledge for a developer to leverage it.
Also by doing this, you do not have to do heavy changes to the AST as part of the complication process and it should be easy to identify branches that are “dead” and can be dropped out of the emit.
Of course this approach doesn’t specifically address conditionality of other language features, like the ability to conditionally load modules or conditional classes, though there are other features being introduced in TypeScript (e.g. local types #3266) which when coupled with this would address conditionality of other language features.
Proposed Changes
In order to support conditional compile time emitting, there needs to be a language mechanic to identify blocks of code that should be emitted under certain conditions and a mechanism for determining if they are to be emitted. There also needs to be a mechanism to determine these conditions at compile time.
Defining a Conditional Identifier at Design Time
It is proposed that a new keyword is introduced to allow the introduction of a different class of identifier that is neither a variable or a constant. Introduction of a TypeScript only keyword should not be taken lightly and it is proposed that either condition
or has
is used to express these identifiers. When expressed at design time, the identifier will be given a value which can be evaluated at runtime, with block scope. This then can be substituted though a compile time with another value.
Of the two keywords, this proposal suggests that has
is more functional in meaning, but might be less desirable because of potential for existing code breakage, but examples utlise the has
keyword.
For example, in TypeScript the following would be a way of declaring a condition:
has hostNode: boolean = Boolean(typeof process == "object" && process.versions && process.versions.node && process.versions.v8);
if (hostNode) {
console.log('You are running under node.');
}
else {
console.log('You are not running under node.');
}
This would then emit, assuming there is no compile time substitutional value available (and targeting ES6) as:
const hostNode = Boolean(typeof process == "object" && process.versions && process.versions.node && process.versions.v8);
if (hostNode) {
console.log('You are running under node.');
}
else {
console.log('You are not running under node.');
}
Defining the value of a Conditional Identifier at Compile Time
In order to provide the compile time values, an augmentation of the tsconfig.json
is proposed. A new attribute will be proposed that will be named in line with the keyword of either conditionValues
or hasValues
. Different tsconfig.json
can be used for the different builds desired. Not considered in this proposal is consideration of how these values might be passed to tsc
directly.
Here is an example of tsconfig.json
:
{
"version": "1.6.0",
"compilerOptions": {
"target": "es5",
"module": "umd",
"declaration": false,
"noImplicitAny": true,
"removeComments": true,
"noLib": false,
"sourceMap": true,
"outDir": "./"
},
"hasValues": {
"hostNode": true
}
}
Compiled Code
So given the tsconfig.json
above and the following TypeScript:
has hostNode: boolean = Boolean(typeof process == "object" && process.versions && process.versions.node && process.versions.v8);
if (hostNode) {
console.log('You are running under node.');
}
else {
console.log('You are not running under node.');
}
You would expect the following to be emitted:
console.log('You are running under node.');
As the compiler would replace the symbol of hostNode with the value provided in tsconfig.json
and then substitute that value in the AST. It would then realise that the one of the branches was unreachable at compile time and then collapse the AST branch and only emit the reachable code.
Issue Analytics
- State:
- Created 8 years ago
- Reactions:329
- Comments:71 (17 by maintainers)
Top GitHub Comments
Hi everyone,
Conditional compilation is a must-have feature in Typescript. The idea of both runtime et precompilation time constants is also a very good idea.
But I think we should use a C#/C++ syntax but adapted to JavaScript :
Sample source
Edit: Remove equals signs to keep C-style syntax as suggested by @stephanedr .
Runtime compilation
This would then emit, assuming there is no compile time substitutional value available (and targeting ES6) as:
tsconfig.json configuration
Assuming you define some constants in your
tsconfig.json
:This would then emit as :
CLI configuration
Or if you define contants in an other way by using CLI :
This would then emit as :
Typings emitting and interpretation
Assuming you have a module designed like this :
Edit: Remove equals signs to keep C-style syntax as suggested by @stephanedr . Edit: Added Class case as suggested by @stephanedr .
If no definitions are configured, it should be interpreted like this :
Edit: Added Class case as suggested by @stephanedr .
If you define some constants in your
tsconfig.json
:Then, it should be interpreted like this :
Edit: Added Class case as suggested by @stephanedr .
It allows compiler and EDIs to ignore some parts of the code based on compiler configuration.
Function-like syntax
Based on @stephanedr comments.
Assuming following sample source
This would then emit, assuming there is no compile time substitutional value available (and targeting ES6) as:
Assuming you define
DEBUG
constant with valuetrue
using CLI ortypings.json
, this would then emit as :Now, assuming you define
DEBUG
constant with valuefalse
using CLI ortypings.json
, this would then emit as :Conclusion
I think it allows a more granular conditional compilation by using the power of JavaScript. Compiler can evaluate expressions passed by compiler directives
#if
#elseif
…Moreover it clearly separates (in both code-style and evaluation) the compiler directives from your code, like it used to be on C-style compiled languages.
What do you think ? Should I start a new issue to avoid confusion ?
I would also like that TS supports conditional compilation, but more in the C++/C# way too. My main use is for assertions. With your proposal @kitsonk, I would be able to empty a function, but not remove its call, e.g.:
With a C++/C# implementation:
Additionally, if TS would provide “macros” for the current filename and line number, I would be able to retrieve them relative to the TS source code, whereas today a stack trace reports line numbers relative to the generated JS file…
Some points from your proposal:
has
, I think we could stay withconst
and allow all constants to be overridable at compile time (and apply dead code elimination with any constant expressions).