Description
Proposal: 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.