Repository navigation
Proposal: Conditional Compilation #3538
Description
Activity
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-elsestatements that are being shaked off at compile time.Reacted by csha, Travis Crist, Frederic Barthelemy, Kerem Kat, sam, vultix, Dante Marshal, Stepan Koltsov, BeA, Isaac and 5 morekitsonk commented
on Jun 18, 2015 ContributorAuthorMore actionsI 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
hasflag 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-elsestatements 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?
Reacted by Eduardo Sanchez- addedSuggestionAn idea for TypeScriptAn idea for TypeScript
on Jun 18, 2015 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 Kitson Kelly (@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) #endifAdditionally, 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 withconstand allow all constants to be overridable at compile time (and apply dead code elimination with any constant expressions).
Reacted by Maxime LUCE, Sfia Daniel-Andrei, interphx, Frank Gambino, Michał Lytek, Jose Alfredo Garcia Guirado, Pauan, Carlos Herrera Fumero, Dasun Sameera Weerasinghe, Tarık İNCE and 18 morekitsonk commented
on Jun 19, 2015 ContributorAuthorMore actionsI would also like that TS supports conditional compilation, but more in the C++/C# way too.
My main use is for assertions.Mohamed Hegazy (@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
constyou 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 (ashasorcondition) 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.Mohamed Hegazy (@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#'sConditional[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
DEBUGis 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. withassert(a() == b()),a()andb()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 asconst, 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.Reacted by Stepan Koltsovkitsonk commented
on Jun 20, 2015 ContributorAuthorMore actionsMy 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.
Richard Simpson (@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.
Kitson Kelly (@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.jsonor.tscrc(same json as defined above).- The mechanism of discovering configuration is usually starting from
cwdtill 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.
Reacted by Sfia Daniel-Andrei and Will 保哥-
- addedNeeds ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.This issue needs a plan that clarifies the finer details of how it could be implemented.
on Dec 9, 2015 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");
Reacted by Thiago Bustamante, Igor Minin, Charles Samborski, penguin020, asurance, sam, AA, Stepan Koltsov and James BromwellHi 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
DEBUGconstant with valuetrueusing CLI ortypings.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
DEBUGconstant with valuefalseusing CLI ortypings.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 ?Reacted by Gábor IMRE, nino-porcino, Tingan Ho, Kasper Kiiskinen, Dasun Sameera Weerasinghe, Tarık İNCE, Marco Medrano, teuf22, Flávio Lisbôa, wmestrom and 19 moreReacted by Marco Medrano, Flávio Lisbôa, wmestrom, Valencio Hoffman, Martin Danhier, PeterGabriel2, Jeremy and Eliseu Monar dos SantosMaxime LUCE (@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()andcast()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;Reacted by Michael Schwartz1/ 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 ?59 remaining items
coolCucumber-cat commented
on Nov 16, 2023 More actionsWhat 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.
HolgerJeromin commented
on Nov 16, 2023 ContributorMore actionsI 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."
coolCucumber-cat commented
on Nov 16, 2023 More actionsHolger Jeromin (@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.
customautosys commented
on Nov 17, 2023 More actionsHolger Jeromin (@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.
Reacted by Tristan Duquesne, Benjamin Lupton, Ricardo Fernández Serrata and BrodBlox09HolgerJeromin commented
on Nov 17, 2023 ContributorMore actionsSame 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.
customautosys commented
on Nov 17, 2023 More actionsSame 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.
Reacted by Ricardo Fernández SerrataReacted by Ricardo Fernández SerrataI 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 likeeval('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. Ifevalis not used, the bundler does not allow to create aumdoutput and must embed all modules in the top-level loader even theimportdoes 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 // ... }
evalis 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.
RyanCavanaugh commented
on Mar 18, 2024 MemberMore actionsA 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.
Reacted by Brian Takita and Claudia MeadowsReacted by Smit Patil and Lesage Yann- addedOut of ScopeThis idea sits outside of the TypeScript language design constraintsThis idea sits outside of the TypeScript language design constraintsand removedNeeds ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.This issue needs a plan that clarifies the finer details of how it could be implemented.
on Mar 18, 2024
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:
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:
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-nodewas in facttrue. The build tool would then realise that theelsebranch 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
conditionorhasis 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
hasis more functional in meaning, but might be less desirable because of potential for existing code breakage, but examples utlise thehaskeyword.For example, in TypeScript the following would be a way of declaring a condition:
This would then emit, assuming there is no compile time substitutional value available (and targeting ES6) as:
Defining the value of a Conditional Identifier at Compile Time
In order to provide the compile time values, an augmentation of the
tsconfig.jsonis proposed. A new attribute will be proposed that will be named in line with the keyword of eitherconditionValuesorhasValues. Differenttsconfig.jsoncan be used for the different builds desired. Not considered in this proposal is consideration of how these values might be passed totscdirectly.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.jsonabove and the following TypeScript:You would expect the following to be emitted:
As the compiler would replace the symbol of hostNode with the value provided in
tsconfig.jsonand 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.