Proposal: Conditional Compilation
kitsonk opened this issue Β· 84 comments
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.
I don't quite like that the proposal isn't addressing conditional imports. I think that is the core issue of having conditionals in the first place.
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.');
}
I'm not sure if I like the runtime property check. We are all developers and we know TypeScript is a compiled language. Why do we need to have a solution when we don't define any flag values? I would rather see it compile as if all conditionals where true then.
I also like the design of conditional flags in C# and C++ because they look like "commented code". Which kind of infer that they only interfere on compile time and not during runtime. They are also simple to understand just like a simple if statement. I guess they are also simpler for the compiler, just let the scanner scan or skip characters depending on if the conditional are true or false. Instead of having a complex tree shaking that removes dead code.
With your solution you are also using runtime statements if-else
statements that are being shaked off at compile time.
I don't quite like that the proposal isn't addressing conditional imports. I think that is the core issue of having conditionals in the first place.
I think that would be a different solution. That is mainly because it is not likely the same pattern can be used with imports without breaking the functionality. The way we addressed this in Dojo, which then worked for both runtime and build time, was to utilise the AMD loader plugin mechanism to evaluate the "magical" MID string expressed as a ternary expression to determine if a certain has
flag value and rewrite the MID appropriately. Because ES6 Modules does not currently solve conditional loading, I assumed (hopefully correctly) that TypeScript would want to wait until that solution is evident before solving that themselves.
I'm not sure if I like the runtime property check. We are all developers and we know TypeScript is a compiled language. Why do we need to have a solution when we don't define any flag values? I would rather see it compile as if all conditionals where true then.
Sometimes a developer will want their code to be emitted as isomorphic, especially if they want to distribute it as a library without the end user having to be aware of TypeScript. This is aligned to design goal "4. Emit clean, idiomatic, recognizable JavaScript code." as well as "7. Preserve runtime behavior of all JavaScript code." and "10. Be a cross-platform development tool." What the runtime code is, is up to the developer and whether it gets emitted or not is up to the developer.
I also like the design of conditional flags in C# and C++ because they look like "commented code".
Maybe you should come up with an alternative proposal. Also, it would seem that that is something that TypeScript has largely avoided, "compiler hints". I dislike "auto-magic" comments/compiler hints personally and seeing it avoided in TypeScript made me happy. You often end up with surprises and the TypeScript Design Goals also state that TypeScript should not "introduce behaviour that is likely to surprise users".
I guess they are also simpler for the compiler, just let the scanner scan or skip characters depending on if the conditional are true or false.
Not necessarily, you will still be modifying the AST if you expect things like intellisense to continue to work. Ignoring written code under certain conditions is never straight forward. I am not sure why you feel comments make this process any easier for the compiler.
With your solution you are also using runtime statements
if-else
statements that are being shaked off at compile time.
Exactly, but only when supplied with compile time values. Why do you feel that is a bad thing?
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.:
has debug = true;
var assert = debugMode ? function (cond: boolean) { if (cond) throw new AssertionError(); }
: function (cond: boolean) { };
With a C++/C# implementation:
#if DEBUG
function _assert(cond: boolean) { if (cond) throw new AssertionError(); }
#define assert(cond) _assert(cond)
#else
#define assert(cond)
#endif
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:
- You seem to consider such a flag as constant, so what is the interest in keeping the 2 alternatives (if/else) when no substitution is given (its value cannot be changed at runtime)? For me the dead code elimination should apply as well.
- Instead of introducing a new keyword
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).
I would also like that TS supports conditional compilation, but more in the C++/C# way too.
My main use is for assertions.
@mhegazy said "Pre-processor directives (i.e.#ifdefs) are not desirable." I took him at his word.
As far as your example, I am a bit lost on how the following would eliminate the function call? Doesn't it generate an assert as a noop anyways, just like you did in TypeScript?
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...
Totally different topic... You do know about the sourceMap compiler option?
You seem to consider such a flag as constant, so what is the interest in keeping the 2 alternatives (if/else) when no substitution is given (its value cannot be changed at runtime)? For me the dead code elimination should apply as well.
There is a big difference between build/compile time value and a runtime value. The intent of my proposal is to allow both, without changing the source code. You can provide all your "feature" logic in your code and then you can choose to have it compiled out, or resolved runtime. In theory, you would always want to have some runtime value. Other examples would be like if you where trying to shim things like Promises or Object.observe. You would write the code to handle both cases, with a clear feature/condition and then you can choose to have that resolved at runtime (and the whole set of code is emitted, including the resolution logic) or you could choose to have two builds, both optimised for the features being there or not.
Instead of introducing a new keyword has, I think we could stay with const and allow all constants to be overridable at compile time (and apply dead code elimination with any constant expressions).
Why I proposed it was because I felt it would lead to less surprises. Again, according to the TypeScript Design Goals, TypeScript should not "introduce behaviour that is likely to surprise users". By leveraging const
you would have to go through all your code and make sure you didn't have name collisions before you were sure that the compiler wouldn't surprise you. While I suspect introducing new keywords isn't straight forward (as has
or condition
) would have to be excised from everyone's code, it would more likely throw syntax errors than actually surprise. I maybe wrong on that aspect though, as I don't know how complex what I am proposing would take to implement.
@mhegazy said "Pre-processor directives (i.e.#ifdefs) are not desirable." I took him at his word.
I just wanted to report my need for conditional compilation, which doesn't seem supported by your proposal. At the end I don't really care of the exact mechanism which might be implemented.
For example, a decorator like C#'s Conditional[DEBUG]
could be fine for me, but this would also require the TS compiler to remove calls to the excluded method.
As far as your example, I am a bit lost on how the following would eliminate the function call? Doesn't it generate an assert as a noop anyways, just like you did in TypeScript?
When DEBUG
is 0 (or not defined), assert(...);
is replaced by ;
, so completely removed (consider text replacement). Compared to a solution which would call an empty function, this also removes its argument(s) (e.g. with assert(a() == b())
, a()
and b()
will be executed, their results compared, then the comparison result passed to the empty function).
Totally different topic... You do know about the sourceMap compiler option?
My goal is not to debug the code under a browser, but to create a mail with the exception stack trace when an uncaught exception occurs at client side.
By having those "macros" I could embed them to exception messages.
There is a big difference between build/compile time value and a runtime value. The intent of my proposal is to allow both, without changing the source code. You can provide all your "feature" logic in your code and then you can choose to have it compiled out, or resolved runtime. In theory, you would always want to have some runtime value. Other examples would be like if you where trying to shim things like Promises or Object.observe. You would write the code to handle both cases, with a clear feature/condition and then you can choose to have that resolved at runtime (and the whole set of code is emitted, including the resolution logic) or you could choose to have two builds, both optimised for the features being there or not.
OK, I misunderstood your example. In has hostNode: boolean = ...
I considered that the expression was constant so simplified by TS as either true or false. So as it was generated in JS as const
, there was no more way to dynamically modify it. If the expression is indeed not constant, you indeed need to keep the 2 alternatives.
@stephanedr Just jumping in, the idea of giving the compiler hints using design-time decorators like @conditional(DEBUG)
has been tossed around more then once and would partial meet the goal of this.
My goal is not to debug the code under a browser, but to create a mail with the exception stack trace when an uncaught exception occurs at client side.
Again, side topic... Mozilla's Source Map would allow you to determine the TypeScript original positions for the source and actually rewrite the stack trace. There are a few other more complete tools out there that leverage source-map and would do the heavy lifting for you. I suspect even with your macros, it would be hard to cover all the transforms that occur during transpilation, where this is more certain.
@RichiCoder1, correct me if I'm wrong, but using decorators will only allow to empty the assert function. What I would like is to remove all the assert calls.
@kitsonk, providing file name / line number should be quite easy for a compiler, as it already maintains them to report compilation errors.
Regarding passing the flags to process, I have a suggestion inspired by some of the node.js based conventions (among which JSCS has aced in this aspect by incorporating every approach):
Note: (in case of redefinitions / clashes) precedence order: descending
-
arguments passed to
tsc
:tsc --cond:FOO1=BAR1 --cond:FOO2=BAR2
-
definition in
package.json
(in case of node.js):"cond": { "FOO1": "BAR1", "FOO2": "BAR2" }
-
definition in
tsconfig.json
or.tscrc
(same json as defined above).- The mechanism of discovering configuration is usually starting from
cwd
till the root of drive and if it is nowhere to found then lastly look into the home directory before warning user and fallback to default settings (or throw "unable to locate configuration").
- The mechanism of discovering configuration is usually starting from
-
environment variables:
FOO1=BAR1
.
Webpack has this feature in the box https://webpack.github.io/docs/list-of-plugins.html#defineplugin
new webpack.DefinePlugin({
VERSION: JSON.stringify("5fa3b9"),
BROWSER_SUPPORTS_HTML5: true,
TWO: "1+1",
"typeof window": JSON.stringify("object")
})
console.log("Running App version " + VERSION);
if(!BROWSER_SUPPORTS_HTML5) require("html5shiv");
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
#define HOST_NODE (typeof process == "object" && process.versions && process.versions.node && process.versions.v8))
#define COMPILE_OPTIONS ["some", "default", "options"]
#if HOST_NODE
console.log("I'm running in Node.JS");
#else
console.log("I'm running in browser");
#endif
#if COMPILE_OPTIONS.indexOf("default") !== -1
console.log("Default option is configured");
#endif
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:
const __tsc__HOST_NODE = (typeof process == "object" && process.versions && process.versions.node && process.versions.v8));
const __tsc__COMPILE_OPTIONS = ["some", "default", "options"];
if (__tsc__HOST_NODE) {
console.log("I'm running in Node.JS");
} else {
console.log("I'm running in browser");
}
if (__tsc__COMPILE_OPTIONS.indexOf("default") !== -1) {
console.log("Default option is configured");
}
tsconfig.json configuration
Assuming you define some constants in your tsconfig.json
:
{
"defines": {
"HOST_NODE": false,
"COMPILE_OPTIONS": ["some", "other", "options"]
}
}
This would then emit as :
console.log("I'm running in browser");
CLI configuration
Or if you define contants in an other way by using CLI :
$ tsc --define=HOST_NODE:true --define=COMPILE_OPTIONS:["some", "default", "options"]
This would then emit as :
console.log("I'm running in NodeJS");
console.log("Default option is configured");
Typings emitting and interpretation
Assuming you have a module designed like this :
#define HOST_NODE (typeof process == "object" && process.versions && process.versions.node && process.versions.v8))
#define COMPILE_OPTIONS ["some", "default", "options"]
export function commonFunction() { }
#if HOST_NODE
export function nodeSpecificFunction() { }
#endif
#if COMPILE_OPTIONS.indexOf("default") !== -1
export function dynamicOptionFunction() { }
#endif
export class MyClass {
common() { }
#if HOST_NODE
nodeSpecific() { }
#endif
}
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 :
export function commonFunction(): void;
export function nodeSpecificFunction?(): void;
export function dynamicOptionFunction?(): void;
export class MyClass {
common(): void;
nodeSpecific?(): void;
}
Edit: Added Class case as suggested by @stephanedr .
If you define some constants in your tsconfig.json
:
{
"defines": {
"HOST_NODE": false,
"COMPILE_OPTIONS": ["some", "default", "options"]
}
}
Then, it should be interpreted like this :
export function commonFunction(): void;
export function dynamicOptionFunction(): void;
export class MyClass {
common(): void;
}
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
#define DEBUG !!process.env.DEBUG
#if DEBUG
function _assert(cond: boolean): void {
if (!cond)
throw new AssertionError();
}
#define assert(cond: boolean): void _assert(cond)
#endif
type BasicConstructor = { new (...args: Object[]) => T };
#if DEBUG
function _cast<T>(type: BasicConstructor, object: Object): T {
#assert(object instanceof type);
return <T>object;
}
#define cast<T>(type: BasicConstructor, object: T) _cast(type, object)
#else
#define cast<T>(type: BasicConstructor, object: T) <T>object
#endif
class C {
f(a: number) {
#assert(a >= 0 && a <= 10);
let div = #cast(HTMLDivElement, document.getElementById(...));
}
}
This would then emit, assuming there is no compile time substitutional value available (and targeting ES6) as:
const __tsc__EMPTY = function () { return; };
const __tsc__DEBUG= !!process.env.DEBUG;
let __tsc__assert = __tsc__EMPTY;
if (__tsc__DEBUG) {
function _assert(cond) {
if (!cond)
throw new AssertionError();
}
__tsc__assert = function (cond) { return _assert(cond); };
}
let __tsc__cast = __tsc__EMPTY;
if (__tsc__DEBUG) {
function _cast(type, object) {
__tsc__assert(object instanceof type);
return object;
}
__tsc__cast = function (type, object) { return _cast(type, object); };
}
else {
__tsc__cast = function (type, object) { return object; };
}
class C {
f(a) {
__tsc__assert(a >= 0 && a <= 10);
let div = __tsc__cast(HTMLDivElement, document.getElementById(...));
}
}
Assuming you define DEBUG
constant with value true
using CLI or typings.json
, this would then emit as :
function _assert(cond) {
if (!cond)
throw new AssertionError();
}
function _cast(type, object) {
_assert(object instanceof type);
return object;
}
class C {
f(a) {
_assert(a >= 0 && a <= 10);
let div = _cast(HTMLDivElement, document.getElementById(...));
}
}
Now, assuming you define DEBUG
constant with value false
using CLI or typings.json
, this would then emit as :
class C {
f(a) {
let div = document.getElementById(...);
}
}
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 ?
@SomaticIT A few remarks:
1/ For people who know C/C# syntax, the "=" sign between the name and the value may be a bit disturbing. Why not keeping the C/C# syntax?
2/ It should allow something like:
class C {
#if DEBUG
f() {}
#endif
}
(for sure, here DEBUG needs to evaluate to a constant to generate valid ES6 code).
3/ It should also support function-like syntax, e.g.:
#if DEBUG
function _assert(cond: boolean): void {
if (!cond)
throw new AssertionError();
}
#define assert(cond) _assert(cond)
#else
#define assert(cond)
#endif
#if DEBUG
function _cast<T>(type: { new (...args: Object[]) => T }, object: Object): T {
assert(object instanceof type);
return <T>object;
}
#define cast(type, object) _cast(type, object)
#else
#define cast(type, object) <type>object
#endif
class C {
f(a: number) {
assert(a >= 0 && a <= 10);
let div = cast(HTMLDivElement, document.getElementById(...));
}
}
The simplest to get assert()
and cast()
managed by Intellisense is to declare them (before their corresponding macro implementations):
/** ... */
declare function assert(cond: boolean): void;
/** ... */
declare function cast<T>(type: { new (...args: Object[]) => T }, object: Object): T;
1/ I agree with you, I edited my comment to remove =
in define.
2/ I also agree with you, I edited my comment to add this exemple in Typings emitting and interpretation part.
3/ I think this case is really interesting but I think we should improve compilation emitting and typings interpretation in this particular case.
I added a Function-like syntax part. What do you think ?
@SomaticIT
3a/ When DEBUG=0, the call is replaced by the given text, substituting the arguments. Fine.
But when it's a non-constant expression, you call functions. Which is not the same because you still evaluate the arguments.
assert(anotherModule.aDebugFunction());
anotherModule
may be not imported in DEBUG mode,anotherModule
may be compiled with DEBUG=0 at compile time, so withoutaDebugFunction()
.- Even if both exist, you may want to not call it in non-DEBUG mode.
3b/ I'm not sure this can always be parsed, particularly with return union types (where the type ends? where the "body" starts?).
I think we'll need a separator (=>
?).
3c/ A same function-like define can be implemented several times (here 2, but might be more). Where should we put the JSDoc (avoiding duplication)?
An alternate syntax might be:
/** ... */
#macro assert(cond: boolean): void {
#if DEBUG
if (!cond)
throw new AssertionError();
#endif
}
/** ... */
#macro cast<T>(type: BasicConstructor, object: Object): T {
assert(object instanceof type); // or #assert if we want to distinguish macros from normal functions.
return <T>object;
}
Note that I'm not especially attached to a "#" syntax, so might also be:
macro assert(cond: boolean): void { ... }
[macro]
function assert(cond: boolean): void { ... }
Sorry I've used the C# decorator syntax. Let's use the TypeScript's one.
@macro
function assert(cond: boolean): void { ... }
My project needs conditional compilation in order to support partial builds - when the user chooses that he wants to have a version of the library that only has components 1, 3, X... in it. While most of that can be supported by splitting the code into separate files, there are some cases when I need to define that certain class members are for one component only. For this I currently have a script that uses the compiler API and uses a regex to apply simple conditional text replacements in the source code.
Some background: C/C++ and C# support preprocessors, Java doesn't. However, one can use the C++ preprocessor system in Java, see this post.
I was somewhat curious about this feature in TS and I think there are valid cases where preprocessors could be used without abusing the TS/JS dev and build process. But I'm not convinced that this should be done during the TS compilation. Gulp offers some preprocessing right now, see preprocess or gulp-preprocess or Webpack's similar feature mentioned by @cevek.
Here is a simple experimenting of mine to check out gulp-preprocess with TS. (Don't expect much.) This preprocessor seems to be kinda useful, not with all of the C++ preprocessor features though.
Currently I see these major weaknesses of external preprocessors.
- They can lead to invalid TS code before preprocessing. This is a real pain, however, in most cases this can be solved, avoided or workarounded.
- The TS compiler won't see the difference caused by them. Changing the interface based on build options seems to indicate bad architecture. (Or at least something being very far from the JS/TS world.)
- Need to use an external build system (e.g. Gulp). But if preprocessors are required then there already should be a fairly complex build system.
So I think that using preprocessors in a TS environment is a very special requirement (i.e. not general) that can be solved by using already existing tools. I recommend reconsidering this feature after at least a half or one year. There are more important and way more general feature requests now.
I propose the following syntax, which is most likely the easiest to implement:
//#if DEBUG
console.log("debug mode");
//#else
console.log("release mode");
//#endif
- Does not introduce additional syntax for a feature many would likely never use
- Does not require an extension to the actual compilation logic, only an additional step when parsing comments
- Developers using VS and Web Essentials will find the syntax familiar
You can use build toggles quite easily : https://basarat.gitbooks.io/typescript/content/docs/tips/build-toggles.html πΉ
The @basarat link is nice
It is common to switch in JavaScript projects based on where they are being run. You can do this quite easily with webpack as its supports dead code elimination based on environment variables.
On the other hand, that approach does not immediately suggest a clear distinction from conditional control flow at runtime, while separate syntax would clearly indicate that the conditions are to be interpreted during compilation, and don't exist at runtime.
Is webpack able to remove function calls, e.g. assert() (for sure without surrounding each call with an if)?
Support of "macros" would also solve requests like #8655.
@stephanedr
@JohnWhiteTB
Let me give you an example of the DefinePlugin
of webpack (sorry if it's strictly out of the initial scare of the issue)
1/ Defined your constant in your webpack.config.js
new webpack.DefinePlugin({
'MY_CONFIG': {
'ENVIRONMENT': 'development'
},
}),
2/ Make Typescript "happy" with this global constant with the custom-typings.d.ts
(or any name *.d.ts)
declare class myConfig {
ENVIRONMENT: string;
}
declare const MY_CONFIG: myConfig;
3/ use it anywhere you want
if ('development' === MY_CONFIG.ENVIRONMENT) {
console.log('development environment');
} else {
console.log('NOT the development environment');
}
in the JS
transpiled code, it will be transformed to :
if (true) {
console.log('development environment');
} else {
console.log('NOT the development environment');
}
and after the minification step, it will become :
console.log('development environment');
"et voilΓ !"
@GabrielDelepine
What about support of "macros"?
The typical example is assert() and cast(), where as TS code like this:
function f(a: number) {
assert(a >= 0 && a <= 10);
let div = cast(HTMLDivElement, document.getElementById(...));
}
would be finally generated as follows in release mode:
function f(a) {
let div = document.getElementById(...);
}
The point here is that if (MY_CONFIG.ENVIRONMENT ...)
are embedded into assert() and cast().
Hi guys, check out this project : https://github.com/domchen/typescript-plus . It is an enhanced version of the original typescript compiler, which provides conditional compilation.
You can use the defines
option to declare global variables that the compiler will assume to be constants (unless defined in scope). Then all the defined global variables will be replaced with the corresponding constants. For example:
tsconfig.json:
{
"compilerOptions": {
"defines": {
"DEBUG": false,
"LANGUAGE": "en_US"
}
}
}
TypeScript:
declare var DEBUG:boolean;
declare var LANGUAGE:string;
if (DEBUG) {
console.log("DEBUG is true");
}
console.log("The language is : " + LANGUAGE);
function someFunction():void {
let DEBUG = true;
if (DEBUG) {
console.log("DEBUG is true");
}
}
JavaScript:
if (false) {
console.log("DEBUG is true");
}
console.log("The language is : " + "en_US");
function someFunction() {
var DEBUG = true;
if (DEBUG) {
console.log("DEBUG is true");
}
}
As you can see, the second if(DEBUG)
in someFunction
is not replaced because it is defined in scope.
Note that the compiler does not dropping the unreachable code, because it is can be easily done by other tools like UglifyJS or Google Closure Compiler.
Here's my idea:
-
Introduce
@@guard(cond) { ... }
and@@guard(version, range) { ... }
constructs that works in most declarative contexts, provided its members are syntactically correct. Additionally, within that block, if it's not matched, only check for balanced parentheses and brackets within template interpolations and outside strings. The top brackets are optional, and each inner expression must be a constant expression (i.e. same restriction as withconst enum
s).The first form,
@@guard(cond) { ... }
, evaluates to its block if and only ifcond
evaluates totrue
.The second form,
@@guard(version, range) { ... }
, evaluates to its block if and only ifversion
is satisfied with a semverrange
.// TS define @@guard(env ts.version, 'version') { ... } // Compile option @@guard(env ts.options.module) { ... } // User define @@guard(env foo.bar) { ... } ```0
-
Introduce a
env customOpt
construct that evaluates to the constant result of that option. The option name must be a valid identifier, custom ones must not bets
, and it returns a constant expression.
This is intentionally made such that it could be resolved at parse time, too.
Cool me old-school (actually, I am old-school), so my vote is for simple, C++ style pre-processor [like] directives to control conditional compilation, with the ability to control those values from the tsconfig.json or (in the case of IDEs like Visual Studio), from the IDE.
I am not a fan of trying to make conditional compilation "look like" program code, and I believe that the most flexible form of conditional compilation, which allows switching in eg. different modules, classes, interfaces, or whatever, needs that conditional compilation to sit "in front" of the TS compilation.
I use C# heavily and I like the ConditionalAttribute behaviour that they have for cases such as removing Debug.WriteLine calls, and I can see it working nicely for Assert-style semantics also, but it is a much more limited capability in my mind.
I'm hitting this problem with a lot of universal code that could be made working in the browser and in node simply by having a:
#IFDEF _CONDITION_SPECIFIED_IN_TSCONFIG
... do something -say- browser-specific, like open a pop-up
#ELIF
... do something that has a meaning only in node, like post the message to an AMQP queue
#ENDIF
Not having this forces to have two quasi-identical classes/functions in the client and the server code.
This, happening in many places, poses serious maintenance problems on a medium code size (>50k lines).
With a tsconfig-driven #IFDEF we could use two separate tsconfigs and each parted build could reference libraries that otherwise would generate globs of compilation errors on the other.
I'd say it's rather simple to implement, since active code can be inferred before compilation.
Badly badly needed indeed.
I see this is lingering since 2014 with ish #449
R
so on our project we need to hide some of the latest features from the release version this is what we do:
// release branch
declare global {
declare const enum NotFor { Release = 0 }
}
if (NotFor.Release) {
// here comes the feature which is hidden in release
}
in the ongoing development branch we use
// develop branch
declare global {
declare const enum NotFor { Release = 1 }
}
if (NotFor.Release) {
// here comes the feature visible in the
}
it's ugly as hell, but we eat it since don't have anything better
Not that this solves it at a TypeScript level, but we (@dojo) have continued to implement a solution that we can use to give us build time optimisation as well. We use a the has API to provide in code feature switching. We then have a webpack loader that will statically replace constructs (as well as elide imports) based on asserted feature flags.
This gives us run-time feature switching we want but also, by replacing the statically asserted features, we can remove imports via the loader and uglify can perform dead code removal inside of modules when the bundle is minified.
Maybe implement it like Haxe: https://code.haxe.org/category/beginner/conditional-compilation.html
Work's good and it's simple and easy to use
Suggestion if implementing preprocessor directives
If this is to be implemented (I'm on the fence -- see below), take a look at the npm package preprocessor. It has desirable syntax mentioned by @JohnWeisz and plenty of features. The only change I would make is being able to configure the path resolver. Currently it is fixed as:
Includes (always relative to the baseDirectory, defaults to ".")
But it would be nice if it used require.resolve()
or a callback as an alternative.
Why do we need this?
Common uses for an #ifdef
are:
- toggle/change an import,
- prevent a reference (to that import); or,
- toggle a behavior (such as trace logging).
Common uses for a #define
are usually complimentary to the above:
- later used in
#ifdefs
without adding constants to code - configuration (where constants are often more appropriate)
- small macro functions (which are inherently dangerous)
And while conditional compilation is a convenient hack, what you really need to keep the code clean long-term is IoC + DI
Currently supported alternative
DI has already been implemented in TypeScript via ES7 decorators (TypeScript link). This prevents import
statements to missing files which, unlike require
statements, cannot be placed within conditionals. Bonus: it also decouples your code and can reduce the number of imports per module.
DI + different sets of source and configuration files covers the majority of cases where conditional compilation would be used.
Steps involved
- Install an IOC container such as decoration-ioc (based on the IOC in
vscode
) or typescript-ioc (which has great readme docs).
(disclaimer: I have not used either in production)
- Enable decorators in your
tsconfig.json
via:
"experimentalDecorators": true,
"emitDecoratorMetadata": true
- You would then configure different sets of sources via
include
+ differing hierarchies of tsconfigs using extends.
Admittedly it's quite a bit of initial complexity for a handful of #ifdefs
but there are major long-term maintainability advantages over conditional compilation and it becomes very pleasant to work with over time (my experience with DI is primarily in C# but the concept is universal).
Input greatly appreciated -- @Nimikolh if you get a chance, please expand on why you believe DI (or the npm
preprocessor
package) are less desirable than other proposals. I'm looking into this setup for an existing codebase so all input is welcome!
@gwicksted The main reason I would like to see this supported as a first class citizen feature in TypeScript is to get the full power of the type system. Being able to turn on / off certain features in my code or to predefine a set of targets can indeed be done easily with any preprocessor. However this is not the only issue that needs to be addressed in my opinion. For instance getting your IDE (or an editor using ts-server) understand that it should be run after the preprocessor is also a valid concern.
So now, would using DI be the solution to rule them all? I don't think so. Assuming the conditional compilation design is driven by the use cases, one could be "how to get the full assistance of the type system while using React Native" for instance. In that example, using DI to resolve a cross-platform component is just overkill. Would conditional compilation be sufficient to make that experience better? Maybe not, given how import
works in that space. However it would definitely helps.
So the TLDR; is multiple platforms.
@gwicksted We are creating a large TypeScript library, and the reason we want conditional compilation is so that we can have two different builds of our library:
-
A build with expensive run-time checking + asserts (for extra safety and correctness)
-
A build without the run-time checks or asserts (for extra performance)
Our users can use the "debug" build while developing, and then when their code is finished, they can switch to the "production" build for extra speed.
This is a common pattern in many languages and libraries (e.g. React and Angular both do this).
We can accomplish this right now with runtime switches, but ideally we would want no runtime overhead at all for the production build. Also, with runtime switches all of the code is included even though the runtime asserts aren't actually being run. So there is a lot of dead code (which bloats up the file size).
We could also accomplish this by having two separate code bases, one for the "debug" build and one for the "production" build. But that's an incredibly bad idea: trying to manage two code bases and ensuring that both code bases remain in sync is impractical.
We could try to use something like preprocessor
, except those tools haven't been designed for TypeScript, so they don't understand TypeScript code. In many cases they don't even understand ES6 code!
Conditional compilation gives us what we want with no overhead. Runtime switches, or DI, or whatever else doesn't work as well.
@Pauan That's understandable since you will have very coupled asserts intertwined with your logic.
Fortunately for you, uglify-es
has drop_console
which will get rid of the entire console.assert
statement including parameters so you can emit a second release-mode file -- which is likely going to be minified already -- by piping it through uglify. Zero overhead, no dead code, and it understands ES6+ code and source maps.
@gwicksted We need to be able to remove our own custom asserts, and runtime checking code, we don't use console.assert
at all.
Also, we don't distribute our library as a bundled + minified + optimized blob of JS code. We distribute our library as ES6 modules, and our users are then responsible for bundling.
So we can't rely upon a minifier which is run in the final stages of compilation, because we can't control how our users choose to compile their code.
@Pauan that makes sense as does not wanting a third party step like a preprocessor/minifier involved.
@Pauan Correct me if I'm wrong, but doesn't React get around this my isolating non-production code in such a way that Webpack and Browserify can shake it out automatically? By wrapping that code in process.env.NODE_ENV
?
@RichiCoder1 I know dead code elimination + tree shaking can do what you are describing and that several transpilers (or a combination of them) are capable of this. Tree shaking is easy thanks to ES6 imports and it looks like dead code elimination has finally become mature enough to use in production.
I'm more familiar with browserify
so I'll take a crack at this with tinyifyify + envify at some point to see how it goes.
At @Pauan 's defense, they want the benefits of this without imposing restrictions on the end user's build system. If TS could perform tree shaking and dead code elimination natively then I'm sure we're all ok with that over a preprocessor!
@RichiCoder1 IIRC, React uses a special __DEV__
global variable that they shake out via either UglifyJS, Webpack, or Babel. (I can't remember exactly which one they shake it out with, though.)
I'd like to add another use-case for this feature that didn't seem to get mentionned yet : shaking out log messages from production builds. The goal would be to allow devs to provide verbose logging for development builds that does not make its way to production builds, thus reducing the package size.
It certainly does not need the full power (and complexity) of conditional compiles, but I am not certain that dead code removal would be able to solve this on its own either.
@Telem yes, it would be much easier with native language support rather than relying on the third-party uglify-es
+ drop_console
(which not only drops asserts that @Pauan requires but also logs).
I have a codebase with custom logging wrappers so shaking them out would require not using those directly (eg. by replacing console
) as well as modifying tslint
rules to allow their usage without warning.
Would also be nice if it wasn't just conditional compilation but also macro functions so non-error logging could include the file and line without specifying them explicitly (verbose) nor generating a stack trace (slow + requires filtering) and would not require a map file to point back to the original TypeScript source from the minified js bundle.
I upvote this as well. My use case is making a TypeScript library that compiles to both NodeJS and Browser dialects. In particular, I would like the ability to alternate between Uint8Array
(which works in the web browser) and Buffer
, which works in NodeJS.
I imagine other people would also benefit from the ability to compile for specific browsers as well.
Upcoming deno has targeted the code to be usable both in node and browser. The creator of NodeJS wanted to remove the whole separation of node and browser.
In webpack there is target option to set particular target platform for the code.
Currently to separate to different platform would be by having two separate project with their own tsconfig files and webpack files. If you have code that is reusable, but need separate platform types, then having one common folder that is imported to platform specific index file or main would be the possible solution.
I was looking into using require.resolve()
to test if a module exists before importing it, but it seems TypeScript is forcing me to require that all modules exist. This thread seems to be the only reference to using it. Has anyone figure out a work-around, or is that yet another limitation of TypeScript? (yes this is doable without using import, but I want to use import to get the types for code completion) I can't even use try...catch because TS does not allow it to be nested in another scope - which I assume is the "conditional" idea if the original post.
I was looking for this ability because I wanted to support importing various database modules WITH their static types, and maintain a list of supported and existing modules internally. I suppose I could manually add the d.ts
types and manually set the types myself, but that seems like a lot of unnecessary work that TS could easily do itself somehow.
@rjamesnw see dynamic imports const modulename = await import("modulename");
since TypeScript 2.4. example here
Alternatively, you can use the require("modulename");
syntax which is synchronous. Since this returns an any
type, you'll likely want to use the import types feature const modulename: import("modulename") = require("modulename");
but only do so if, for whatever reason, you cannot use ES modules.
You would need two parts:
-
Some sort of
if
construct that can be removed at compile time depending on the value of the condition. This means that the condition would need to be knowable at compile time. So basically just simple primitives that can be evaluated as truthey or falsey. If TypeScript supported some type of compile time execution more complex expressions could be evaluated, but that's a different discussion. One work around for this at the moment is to use one of the many existing bundler plugins that can preprocess the code. -
Top level await + dynamic imports. Done.
This would be very useful for targeting different platforms when necessary, such as React Native differences between iOS and Android.
This issue already lives now for more than 5 years and many duplicates/alternatives have been closed already. I was wondering if somebody is using an unofficial alternative in their projects successfully. I personally can imagine some variants to implement this but I would like to avoid re-implementing the wheel:
- Use ttypescript to transform the AST based on comments
//@ts-if DEBUG
(scoped or symbol level), decorators@conditional("DEBUG")
, or comparable. - Emit clean JS from TypeScript, then use a bundler like WebPack or Rollup to do this job in a plugin.
- Use different tsconfigs with different includes and rely on dead code elimination combined with a DI-alike approach.
- anything else?
My direct use case is that I have some "debug" mode of my library where I render some additional guides and hints for better testing and analysis purposes. This feature is only reasonable during development time and I want to remove this code on production for a smaller footprint.
@Danielku15 couldn't agree more! Currently we're running into the same issue but there's no clear path to resolve it.
The most confusing part is that: Optimizers like https://github.com/webpack-contrib/terser-webpack-plugin can provide functionalities like removing dead code. But when it comes to the combination of webpack && TypeScript && terser, it does not work at all.
I believe if we're using JS rather than TS, things can improve a bit. However, we need to move forward on this topic for better TS toolchain.
Hello,
I would like to know if we can reopen the discussion about preprocessor directives: #3538 (comment)
Thanks
I would like to see this functionality to compile project for different target web servers.
My personal instinct after reading much of this thread is that there a desire to build the "Taj Mahal" of conditional compilation when I think for a lot of people, simple and basic conditional compilation would be a fantastic first step.
I dislike how the capabilities of C#/C++ are being conflated together. What C++ can do, relative to C#, makes them two somewhat different beasts.
C# provides a straightfoward means via #if/#endif directives to include, or not include, code.
For me, this might be debugging code in a debug build, or some internal validation code in a debug build, and stuff like that. For others it might be some conditional compilation around web server support.
Because the C# style of behaviour acts like a preprocessor, it works for all portions of a code file, and so can target import statements, class definitions, or whatever else you wish.
My general opinion is this feature is being over-thought and over-baked. If the C# example tells us anything, it's that even a basic capability (which is what C# offers relative to C++) still can be immensely useful relative to nothing at all.
Currently, I'm using Tree Shaking in webpack (https://webpack.js.org/guides/tree-shaking/) and the DefinePlugin to implement condition build for different targets: Android, iOS, web, test.
You could turn off sideEffects to make sure unused imports are not bundled: https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free
I just wanna express the need I'm having right now for conditional compilation based on the target "module"
. If I could have a slightly different output for ESM and CJS would be perfect.
#if tsconfig.compilerOptions.module === "ESNext"
const script = await import(filename);
#else
const script = require(filename);
#end
Perhaps all we need is a way to provide custom variables at the compilation level (+ default ones such as tsconfig options) and filter out AST branches that do not meet special conditional expressions. Branches filtered out do not need to be checked/compiled. I understand this is quite complex though!
I'd like to give a shout out to the https://github.com/jarred-sumner/atbuild
I use it for a few projects to generate TypeScript using TypeScript and cases like the above are easy to do.
Conditional compilation could also be used to provide conditional exporting so that items can be exported only when running in development mode for the purpose of unit testing, while not being exported in production code.
For example:
export function ComplexFunction(args: ComplexFunctionArguments) {
...
}
#if DEBUG
export
#endif
function HelperFunction(args: HelperFunctionArguments) {
...
}
That way, unit test code (which will always be running in development mode) can directly call/mock/etc HelperFunction
but attempting to import/use HelperFunction
directly when running in production mode will be a compile-time error.
I came across a situation where I wanted to do a conditional compile and searching led to this this open issue.
Node.js 14.17.0 introduced a new "randomUUID" function. in the "crypto" package. As a built-in now, a module is no longer required and I prefer to use built-in's wherever possible. Prior to this new built-in, I was referencing the "v4" function of the "uuid" package on npm. The problem I am having is one module I have runs on an old Node.js 8.17.0 system as well as newer Node.js 14 and 16 systems. It would be nice to have the TypeScript code compile one way for the ES2018 target (Node.js 8) and another for ES2020 target (Node.js 14+) which I compile using to different tsconfig.json files. For now I just hand edit the one built module that could use the conditional option so that it is one of these statements:
const { v4: randomUUID } = require ("uuid"); // Node.js 8
const { randomUUID } = require("crypto"); // Node.js 14.17+
This is obviously a very minor case but it could be more complex issue for other people as I read through the comments.
Strongly support, especially needed for cross platform (e.g. Electron / Cordova)
I agree with @customautosys.
Nowadays, targetting multiple environments is a common task in large SPA/PWA.
As a reminder, I put again a link to my proposal:
#3538 (comment)
Would it not be better for this to be implemented as a form of triple-slash directive? This would be much like we have with references and the amd module stuff. Something like:
doGeneralStuff()
/// <conditional platform="election" ...other props perhaps... >
electrionOnly()
/// </conditional>
/// <conditional platform="node" ...other props perhaps... >
nodeOnly()
/// </conditional>
doMoreGeneralStuff()
This syntax wouldn't break parsers like eslint's typescript parser and countless other projects, it's already familiar to those who have used triple-slashes in typescript and have used xml or a like. I also like it allows for the use of more than one condition specified as a prop on the conditional node. How this ties into the tsconfig and what parts are dynamic I'll leave that to the better of the conversation here.
Just an off the cuff thought I wanted to share and see what you all thought.
This proposal has been long-standing (years).
Still it could be useful to have an embedded such mechanism.
R
Would it not be better for this to be implemented as a form of triple-slash directive? This would be much like we have with references and the amd module stuff. Something like:
doGeneralStuff() /// <conditional platform="election" ...other props perhaps... > electrionOnly() /// </conditional> /// <conditional platform="node" ...other props perhaps... > nodeOnly() /// </conditional> doMoreGeneralStuff()This syntax wouldn't break parsers like eslint's typescript parser and countless other projects, it's already familiar to those who have used triple-slashes in typescript and have used xml or a like. I also like it allows for the use of more than one condition specified as a prop on the conditional node. How this ties into the tsconfig and what parts are dynamic I'll leave that to the better of the conversation here.
Just an off the cuff thought I wanted to share and see what you all thought.
That's also OK. It doesn't really matter what the syntax is as long as there is a way to select / remove code at compile time based on a compile time constant / definition. Whether it's /// or #define / #if / #ifdef / #ifndef doesn't matter.
@customautosys It does matter a little. If existing tokenizers can consume the code without exploding that is a win. Things like linters don't need to be aware of conditional behavior to do their job but they can't do it at all if they can't parse the code. There are many other kinds of tools that would run into similar problems I'm sure.
I'm really not a fan of the ///
thing. Magic comments are always a bad thing.
Whenever TypeScript adds a new syntax feature, existing parsers need to be updated, that's just a fact of life.
TypeScript adds new syntax fairly often, the most recent example is template string types in 4.1
I thought about it a bit more, and I've come around on the syntax. The triple slash stuff is currently used for js files where typescript syntax can not be used.
I just re-read it and it seems the proposal does not support conditional compilation for imports. This makes it a lot less useful then. We should have a way to allow for conditional imports.
There are already people doing something similar, but it's non-standardised and people are doing different things for webpack, vite etc.
https://www.npmjs.com/package/ifdef-loader
https://www.npmjs.com/package/vite-preprocess
We need a standardised way to go about it so the code will not be brittle.
Magic comments seem to be the way to go. Coming from a C++ environment, I think the #if / #else syntax is something a lot of people are familiar with.
This seems like such a trivial thing to implement. The #ifdef/#if/#else/#elsif/#endif kind of syntax (or use @ instead of #) is fine.
I really want to put #ifdef MIKE_REMOVED_THIS ... #endif around a big block of code to comment it out, and /* / doesn't work because there might be / style comments within the block. The MIKE_REMOVED_THIS is a way to blame MIKE for the elimination of the code - for other contributors to see.
At the top of the file, I would put:
#define MIKE_REMOVED_THIS
#undef MIKE_REMOVED_THIS
and comment out the undef to remove the code throughout the file where the #ifdef is used.
If you have to provide #define and #undef, then so be it. It would be fine. You don't have to allow #define values to substitute within any of the TS code, just use it for #if conditionals.
It really should be a feature of the compiler/language, not some hack using a rollup/webpack layer that may not be used at all (like a server-side program).
What about something like Rust macros? It just runs a compile time and returns some code, which then replaces the macro. Which would be kinda overkill and too complex just for this issue, but macros would be nice anyway and it might actually be worth it.
I think I can write exactly the same as I did for the macro idea. The current design goals of Typescript are:
Goals: "Align with current and future ECMAScript proposals."
As far as I can imagine: It is not even possible to create an ECMAScript proposal for that because there is no compilation in ES.
Non-Goals: "Exactly mimic the design of existing languages. Instead, use the behavior of JavaScript and the intentions of program authors as a guide for what makes the most sense in the language."
@HolgerJeromin Same can be said for this entire issue. Conditional compilation also doesn't align with ES because compilation isn't in ES. The only reason I said this is because clearly people don't think that's an issue here.
@HolgerJeromin Same can be said for this entire issue. Conditional compilation also doesn't align with ES because compilation isn't in ES. The only reason I said this is because clearly people don't think that's an issue here.
That's taking the argument to its logical extreme. Strict typing isn't in ES either, do we then say types don't align with ES even though they're the raison d'Γͺtre of Typescript? ES is not compiled but I think TS was designed to be compiled / transpiled since its inception. And that means that effectively zero runtime cost compile-time features like conditional compilation can be implemented in Typescript. The fact is that this has already been implemented in several plugins for various tool stacks in npm (e.g. https://www.npmjs.com/package/vite-plugin-conditional-compiler); this feature would just standardise the reality so that it will not be a mess.
Same can be said for this entire issue.
That's taking the argument to its logical extreme.
No. That was exactly my point (as it was for the macros suggestion).
Conditional compilation is out of (documented) scope/goal of typescript.
Strict typing isn't in ES either, do we then say types don't align with ES even though they're the raison d'Γͺtre of Typescript?
There are 11 typescript goals. Not only my quoted 2.
Same can be said for this entire issue.
That's taking the argument to its logical extreme.No. That was exactly my point (as it was for the macros suggestion).
Conditional compilation is out of (documented) scope/goal of typescript.Strict typing isn't in ES either, do we then say types don't align with ES even though they're the raison d'Γͺtre of Typescript?
There are 11 typescript goals. Not only my quoted 2.
Wouldn't it be compliant with these 2:
Provide a structuring mechanism for larger pieces of code.
Impose no runtime overhead on emitted programs.
I don't think the aim of the proposal is to mimic other languages exactly? I think the main aim is to structure the inclusion of specific pieces of code at compile time which is really useful for large cross platform codebases e.g. when you need to import 1 platform specific module only for a certain platform. Sometimes dynamic imports are not suitable.
There's another caveat, one that probably would be a death sentence to this proposal: these stated non-goals.
- Provide an end-to-end build pipeline. Instead, make the system extensible so that external tools can use the compiler for more complex build workflows.
- Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.
- Provide additional runtime functionality or libraries. Instead, use TypeScript to describe existing libraries.
I don't see how those would spell a death sentence for this proposal.
You could argue that this is out of scope and therefore 4 would rule it out, but there are already a bunch of competing third party solutions for this problem. It would be nice if something like this could be handled at the language level. That would remove the need for weird solutions and would make it easier to change build tools. If, for example, a codebase makes heavy use of something like webpack define plugin then it's difficult to switch to a different bundler unless there is a compatible solution available there.
5 and 6 don't seem relevant. Unless I'm missing something. The core of this proposal is conditional emit. i.e. I want to be able to set some flag internally or externally (via compiler flags, env vars, or some other mechanism) that controls which bits of code are emitted. It doesn't add any reliance on runtime type information nor add any extra runtime functionality.
There are already people doing something similar, but it's non-standardised and people are doing different things for webpack, vite etc.
https://www.npmjs.com/package/ifdef-loader https://www.npmjs.com/package/vite-preprocess
We need a standardised way to go about it so the code will not be brittle.
Magic comments seem to be the way to go. Coming from a C++ environment, I think the #if / #else syntax is something a lot of people are familiar with.
Magic comments would also work with js files without having to go through years of ES standard deliberation & runtime implementation.
I'm facing the question of how to use conditional compilation to reduce browser payload size. There are several esbuild plugins. It would be great to know what the blessed syntax is so I can target it. I vote for magic comments because they can be done today. Magic comments can provide a prototype for additions to js & ts.
I like how the preprocess library approaches this because it can be used for various file types, including css, shell, php, etc. Also, since the code to be added by the preprocessor is in comments, there will be less build issues for when not running the preprocessor. Perhaps there is potential standard approach for preprocessing any programming language which supports comments?
What about something like Rust macros? It just runs a compile time and returns some code, which then replaces the macro. Which would be kinda overkill and too complex just for this issue, but macros would be nice anyway and it might actually be worth it.
I swear I think about this stuff every week. I've desperately wanted something like Rust or Haxe macros in TS since day 1. I do understand some of the arguments against though, it pains me to say. The biggest one for me is imagining the intersection of JS and TS libraries which is a hellish forest on average and can't possibly be improved by a bunch of library/project specific flags and compile time function evaluations. I imagine build times suffering for reasons that could be difficult to intuit, or hunting desperately for The Flag That Is Breaking The Build And How.
The best argument for is that macros and conditional compilation is phenomenal and even hard to imagine working without if you own all or most of the code. Lots and lots of valid arguments against, in our case, and it sucks.
If TS were to implement conditional compilation or macros I'd want it as a first class language feature with no magic comment ambiguity and code completion support, it just seems silly to compromise if you are going for it because you are worried about overly complicating the AST for instance (this is pretty rote text processing and shouldn't touch the AST other than choose what gets emitted into it). I'd look at the Haxe implementation. It is simple enough yet covers nearly every practical base and can be extended through exposing of compiler macros if the team were ever to desire such madness.
I just re-read it and it seems the proposal does not support conditional compilation for imports. This makes it a lot less useful then. We should have a way to allow for conditional imports.
I found a workaround for conditional import
. It is something like eval('import("xxx")')
. My context is to switch between development mode and production mode. In development mode, modules should be loaded with HMR (hot module replacement) enabled. In production mode, all modules are bundled in a single javascript file. If eval
is not used, the bundler does not allow to create a umd
output and must embed all modules in the top-level loader even the import
does not actually have a chance to execute.
The following code snip is as used in a project based on vue+vite.
async function importModule(name: keyof AdminViews) {
if (import.meta.env?.DEV_MODE) {
return eval('import("./views/"+name+".vue")')
}
// in production mode, dynamically load it from a bundled javascript file
// ...
}
eval
is not of perfection and the bundler complains,
Use of eval is strongly discouraged, as it poses security risks and may cause issues with minification
For a workaround, it's better than none.
A few different aspects on this one
Conditional compilation in value-space (statements and expressions) is pretty clearly out-of-scope in the modern understanding of TS; this is basically the same as "macros" which got closed a while back.
Something like conditional code exists in NodeJS's "conditional exports" https://nodejs.org/api/packages.html#conditional-exports which let you expose different code depending on external factors.
Something akin to conditional compilation in type space -- e.g. "this interface has this member if some condition is met" or "this variable exists if (some other condition is met)" - I would still consider to be tenable. There are multiple open problems that might be well-solved by a mechanism like this, but it's an extremely blunt tool that would certainly have a lot of side effects, like creating difficulties in validating whether a .d.ts file is even valid, avoiding circularities, and keeping features like "rename" functional in code that is conditioned out. I would consider this to be a "we have tried literally everything else" sort of solution to problems like webworker vs dom environment code living in the same compilation unit.
So overall this is either out of scope / some other tool's job, or better phrased as an extremely minimal proposal for something in type space.