Repository navigation
Support conditional compilation #449
Description
Activity
RyanCavanaugh commented
on Aug 18, 2014 MemberMore actionsWe'd like to gather some use cases on this. What do people want to use this for? There might be better solutions than conditional compilation depending on the application.
We would likely not implement a
ConditionalAttribute-style attribute as it violates the "Don't depend on typechecking for emit" design goal.For the
console.logcase, adding conditional compilation would be a large hammer for a small nail. Doing this no-op'ing at runtime would be essentially zero overhead (assuming that you would be writing a human-readable amount of stuff to the console in the debug case).Reacted by NatesworksI can think of a few cases:
- Being able to control which parts (files) of the project are subject for a build. One can use directive constants like tags. This would allow slicing and dicing the project any way you like and focusing on certain pieces while doing refactoring. Currently in order to focus on a certain piece, say the data layer, without having to deal with cascading breaks across all layers, one has to exclude the unrelated files before and reincluding them all back after when the refactoring is done, which is a hassle. Such problem would not exist if VS had support for special TypeScript only projects that would be linked all together and could be addressed one-by-one separately.
- Enabling assertions, debugging along with all sorts of backdoors for non-production versions. While, as you say, tracing can be done without conditional compilation with debugging it is different, because it takes more granular control over the code. Also for performance critical code conditional compilation is the only option.
Reacted by athinboy, Evgeniy Timokhov, Oliver Janik, Dag Christensen, Martin Braun, Yawar Amin, Kevin Ralphs, Asriel Pd Santos, Boris Prpic, Mahmud Ahmad and 4 moreSome use-cases:
- Variable defintion
// Production sources and keys var foo = { root: "https://yyy.blob.core.foobar.net/", googlePlusKey: { id: "888888", key: "GIzdfBy" }, facebookKey: { id: "444444444" } }; // Development sources and keys #if(DEBUG) var foo = { root: "https://xxx.blob.core.foobar.net/", googlePlusKey: { id: "458588", key: "BIzdfGy" }, facebookKey: { id: "123219585123132" } }; #endif
- Import statement
#if(DEBUG) import foo = require('debug'); #else import foo = require('release'); #endif function doFoo(){ foo.someMethod(); }
- Class definition
#if(DEBUG) class Foo { doFoo(){ console.log('debug'); } } #else class Foo { doFoo(){ console.log('release'); } } #endif var foo = new Foo();
Reacted by Gadhami, athinboy, Andrew Street, Dag Christensen, Menushka Weeratunga, Martin Braun, amb-jarek, Thomas ten Cate, Rafael Guerreiro, PinkiePieStyle and 25 moreCyrusNajmabadi commented
on Aug 27, 2014 ContributorMore actionsHaving worked on the Roslyn API support for the C#/VB Preprocessor, I would def like to avoid bringing those huge complexity farms to TypeScript.
I am amenable though to other mechanism that might achieve the same goals, albeit with less flexibility.
-- CyrusFrom: nabog [mailto:notifications@github.com]
Sent: Sunday, August 24, 2014 3:41 AM
To: Microsoft/TypeScript
Subject: Re: [TypeScript] Support conditional compilation (#449)Some use-cases:
- Variable defintion
// Production sources and keys
var foo = {
root: "https://yyy.blob.core.foobar.net/", googlePlusKey: { id: "888888", key: "GIzdfBy" }, facebookKey: { id: "444444444" } };// Development sources and keys
#if(DEBUG)
var foo = {
root: "https://xxx.blob.core.foobar.net/", googlePlusKey: { id: "458588", key: "BIzdfGy" }, facebookKey: { id: "123219585123132" } };#endif
- Import statement
#if(DEBUG)
import foo = require('debug');#else
import foo = require('release');#endif
function doFoo(){
foo.someMethod();}
- Class definition
#if(DEBUG)
class Foo { doFoo(){ console.log('debug'); } }#else
class Foo { doFoo(){ console.log('release'); } }#endif
var foo = new Foo();
—
Reply to this email directly or view it on GitHubhttps://github.com//issues/449#issuecomment-53187591.Reacted by Janis Pütz, Mahmud Ahmad, fractalmuse, Eduardo Sanchez and NatesworksI think compiler directives would be useful in scenario when you are targeting different platforms and want to have as much shared code as possible. Similar to what compiler directives are used today in C#. Many libraries (in .net framework code too) have code with conditional compiler directives to maximize code reuse.
In JavaScript it's somewhat easier to detect if given feature is available but still there are situation when differences would be better handled by preprocessor directives for example for very different platforms (mobile, set-top-boxes, smart tv). It's sometimes easier to use compiler directive when there are differences in APIs and their behaviours.We would likely not implement a ConditionalAttribute-style attribute as it violates the "Don't depend on typechecking for emit" design goal.
Are you afraid that sometimes it might be not clear that compiler have all type information about call (like in discussion about extension methods?
Well, for people that use noImplicitAny this won't be a problem. We are also not doing any complex code transformation. Just removing some code calls so result JavaScript code would still be readableConditional compilation would definitely be useful in some situation. There might be other solutions but preprocessor directives are very simple to use and might be the best (and simplest) solutions.
But I must admit that the more I think about this feature, the more I consider it less important and would understand if compiler team would focus on other higher priority features.
Reacted by Bobby Galli, Mahmud Ahmad, Eduardo Sanchez, zhengyi and Ricardo Fernández SerrataRyanCavanaugh commented
on Aug 28, 2014 MemberMore actionsTo be clear, "noImplicitAny" is not "noAny"
[Conditional('debug')] declare function debugPrint(s: string); var d = { n: debugPrint }; var x: any = d.n; // Legal even with noImplicitAny x('hello'); // Call to 'x' will not be conditionally compiled
It looks like "full" conditional compilation with preprocessor directives is a complex thing to implement, so maybe it's not worth the effort.
But is it difficult to implement something like ConditionalAttribute so it would be easy to strip-out certain function call in code?We would likely not implement a ConditionalAttribute-style attribute as it violates the "Don't depend on typechecking for emit" design goal.
A lot of minifiers allows to exclude console.log calls and this is often searched topic.
I understand your resistance to this feature, but I believe many will find it useful. It won't be very general and often used feature but if the costs of introducing this feature is relatively small (it might not. I just guess. I don't implement compilers.) than I think it's worth considering to add.About syntax. I guess we don't need special syntax for attributes, something like "special comment" will be fine for me if that would be easier to implement:
/// [Conditional("debug")] declare function debugPrint(s: string);Reacted by John Weisz, Eduardo Sanchez and Piotr RajniszI have just found another one good use case for this feature. I want to to enable JSON Schema validation for API calls. And, of course, I want it only for dev and test builds.
There are two aspects of the problem:
- I need to include modules with these schemas conditionally (I know that it can be done with async loading, but it is much more complex).
- I need an ability to enable/disable some statements that make Schema checks.
I'm writing a lot of code in Rust language and it has a suitable solution:
- Any item can be marked with
#[cfg(...)]attribute to enable or disable this item (example):
#[cfg(debug)] mod test { // ... }
- They have the
cfg!macro that can be used to disable some statements in code:
let my_directory = if cfg!(windows) { "windows-specific-directory" } else { "unix-directory" };
After this RFC will be done,
#[cfg()]attribute will be allowed to use with any code block.So there are good start steps for TS:
#[cfg(validate_schema)] module schemas { export var schema1 = { // ... }; }
export class UserApi extends ApiBase { static authenticate(login: string, password: string): Promise<{user: models.CurrentUser}> { var promise = this.request('/api/auth', { type: 'POST', data: { login: login, password: password } }); #[cfg(validate_schema)] promise = promise.then(validateSchema); return promise; } }
What do you think?
Reacted by Ricardo Fernández SerrataStanislav Panferov (@s-panferov) : it's certainly another solution, but there is potentially more code duplication than with standard pre-processor directives. I have to say that I don't understand the resistance to include them, since TypeScript can leverage the power of the compiler. It doesn't seem logical to have to jump through workarounds that scripted languages by nature need when there is an obvious and simple solution available.
Paul Reilly (@paul-reilly) budget limit a good excuse for resistance, although I am in
your team and i say there are numerous ways to use it, although many of
them can be solved just by leveraging constants say we declareconst debug = falsewhich gives the runtime a strong hint to optimise things likeif (debug) {...}up to exclusion which, at least for me, covers 90% of use
cases
On Dec 11, 2014 5:33 PM, "paul-reilly" notifications@github.com wrote:Stanislav Panferov (@s-panferov) https://github.com/s-panferov : it's certainly another
solution, but there is potentially more code duplication than with standard
pre-processor directives. I have to say that I don't understand the
resistance to include them, since TypeScript can leverage the power of the
compiler. It doesn't seem logical to have to jump through workarounds that
scripted languages by nature need when there is an obvious and simple
solution available.Reply to this email directly or view it on GitHub
#449 (comment)
.How about web.config somehow? My company said it's a bad practice to use #Debug or #Release cuz it deal with processors, so my company require us to use web.config for that purpose. (The trasnformation to web.config takes care of it).
I would definitely like to see conditional compilation in TypeScript. I have a use case for it right now, where I'd like to enable some JavaScript code for when developers run our application, but not include the code at all when it is deployed. Conditional compilation would be ideal for this.
for me it would be very useful for:
- targeting different platforms, like browser vs node
- unit testing. Sometimes it simplifies unit testing a lot when I can just add public properties or functions to a classes with a #TEST condition.
Reacted by athinboy, Oliver Janik, zeas2, Stanislav S. Yarmonov, Bobby Galli, Paul Slocum, Mikis Woodwinter and Todd Powers70 remaining items
Did you consider my suggestion above for using compiler optimisation as a means to (a) preserve existing syntax and semantics (b) perform conditional compilation using constant expression evaluation and dead code elimination in if statements?
Simplest example:if (DEBUG) { console.debug("something") }
Compiler would evaluate DEBUG constant at compile time and if
falseynot output the if statement body.How do you know its constant and isn't mutated? This is the problem Prepack was trying to work through and AFAIK is dead in the water because its HARD
Yes i can understand that in the general case you can't make this assumption but what you can do it tell the compiler via a config option that it's safe to make this kind of assumption and consequent optimisation.
This is more of a question than a feature request, but I am interested in doing some C#/C style macros to transliterate if/then/etc into cyrillic/ukrainian so that developers can develop in their native languages without switching back and forth into english keyboard charsets to input certain characters. Is this already possible?
Reacted by ZM- Switching keyboard can be mapped. You can also use safe hooks of editor. Unlikely to be related to this topic ? Maybe need be one. If it's a real pain point there are more options like auto hotkey contact me.
Marc Weber (@MarcWeber) I think you may have the wrong issue!
I solved my issues by having a base config for development only, then doing multiple passes of different configs that change the include setting.
Oh god, please not
#if. It's a trifecta of awfulness:- it tends to be very limited (e.g. in C, you cannot write conditions that refer to the program's types, functions or variables)
- the engineering effort to implement it (including syntax highlighting in VS Code etc) would be high
- the syntax/semantics of
#ifare strange for people who aren't used to it, and code using it tends to be ugly
I think people want two different things from this:
Conditional methods like
debugLog(...)that vanish in productionHere the goal is to avoid a performance penalty. It seems like you can't completely accomplish this with "#if" inside the definition of
debugLog, since there may be a cost to call a completely empty function ... or would modern JS runtimes fully optimize that away? My guess: JS runtimes can optimize away calls to empty global functions, but maybe not empty methods.Vanishing 'if' blocks, like
if (process.env.NODE_ENV !== 'production') {...}This exact syntax is already supported by the create-react-app toolchain (not sure which part of it).
To me, the obvious way for TypeScript to support this is at the type-system level, so you could write things like
type VersionIsAtLeast<PackageName extends string, V extends number> = (magic expression for checking the major version number of a package); if type (VersionIsAtLeast<"react", 18>) { import { Suspense } from 'react'; export function Foo() { use_react_suspense(); } } else if type (compilerOptions<'target'> extends 'esnext') { export function Foo() { use_esnext_feature(); } } if type (tryGet<DEBUG, true>) export function bar() { console.log('debug_version'); etc(); } else export function bar() { etc(); }
I'm assuming:
- You can call
Foo()outside theif typeblock (its braces are deleted) - There are intrinsics like
compilerOptions(from tsconfig.json) anddependencies(from package.json) tryGet<T, Alt=unknown>is a magic error-suppression intrinsic that returnsAltif symbolTcannot be foundX extends YmeansX extends Y ? true : false(this need not be limited to theif typecontext)if type (X)meansif type (X extends (false | 0 | null | undefined | ''))except that it shows an error if X isneverorunknownor ambiguous (both truthy and falsy, e.g.anyorbooleanor a generic typeT).
For a feature like this to work, the way TypeScript gathers type information must be publicly defined, so we can answer questions like "does the following code show an error that
Xis not defined?"if type (X) { function foo() {} } type X = true;
And what if
Xis defined in another module 'x', and 'x' also imports the current module?It would be nice if this was bundled with other type-level operators like
type Untrue = 3 < 2because this kind of feature would make people try stuff like that.Reacted by Ricardo Fernández Serrata and Zireael07We're not advocating that we implement #if / #define etc, just that we find a reasonable method to extend TS to support the use cases that a C/C++ preprocessor fulfilled.
The solution you've proposed only offers different methods of expressing the condition but it fails to solve the fundamental problem of import scope that I explained in my examples above.
Once you make this assumption...
"You can call Foo() outside the if type block (its braces are deleted)"
... you've broken backwards compatibility and fundamentally altered the semantics of the TS scoping rules.
No,
if typeis a fundamentally new construct so there's no backward compatibility to break. (But if it would make you feel better, it could be called#ifwithout taking on any of the other syntactic or semantic baggage from C/C++)I think one of meaningful use case is we can use it for feature flag.
#if FEAT_1_ENABLED // the feature code #end
Reacted by Matthew NicholsIn C#, I use
#ifa lot when refactoring or fixing bugs. I prefix the conditions withTBR(to be removed). When I'm done, I simply search for#if TBR, collapse the block and delete it. In TypeScript, I use block comments, but they cannot be nested.#if... #elsealso allows me to keep the old code around for comparison (we're talking about a few lines here, not pages of code).#if TBR_OLD_CODE ... old code ... #else ... new code ... #endifI use this for quick, small edits and it really helps me to keep me in the flow when developing.
There are cases where type-driven conditional-compilation could be useful: JS
reducecan't know the expected type of an empty array (which TS can know), so the only disambiguator is an initial value. But this is a catch-22 because to provide a sensible init you need TS knowledge of function call-sites, which we can hack by hard-coding the overloads to force the dev to provide such information (this feels like manual monomorphization... yikes)Meanwhile, other langs:
//edition = "2021" /* This is somewhat unfair, because the TS snippet didn't `import` 3p deps. But (AFAIK) no TS module can remove the extra param, so this is still a "win" for langs like Rust */ use num_traits::Num; // 0.2 /* We could use `core::Iterator::sum`, but `num_traits` supports a wider set of types (including big-nums), so we define our own `sum` */ // off-topic: is `clone` unavoidable here? fn sum<T: Num + Clone>(a: &[T]) -> T { // no `dyn`amic dispatch! a.iter().fold(T::zero(), |acc, x| acc + x.clone()) } // same result if we use `let` inside `main` const I: [u8; 0] = []; const F: Vec<f32> = Vec::new(); // fancy! fn main() { sum(&vec![0, 1, 2, -1, 3]); // ints ✅ sum(&[0.0, 1.1, 2.5, -1.9, 3.333]); // floats ✅ assert_eq!(sum(&I), 0u8); // ✅ assert_eq!(sum(&F), 0.0f32); // ✅ }
Wouldn't it be more interesting to use tools like Terser on top of TS ?
TS responsibility is to type check. Optimizing code by removing debug stuff (and other) is more of the responsibility of tools like Terser, Webpack, etc.
Reacted by snarbles2Reacted by Ricardo Fernández SerrataUsing Vite for conditional bundling.
Many TypeScript apps now use Vite that supports conditional bundling via
import.meta.env. Here's some use case examples:Client & server use case
A classic use case is apps that need separate server-side and client-side logic when implementing SPA / SSR rendering:
if (import.meta.env.SSR) { // server side const serverOnlyModule = await import('./server-only-module.js'); // ... use serverOnlyModule } else { // client side const clientOnlyModule = await import('./client-only-module.js'); // ... use clientOnlyModule }
Conditional Features
If you need to create different versions of an app (e.g. Free vs Premium) or different customer builds you can use a
VITE_prefixed environment variable:if (import.meta.env.VITE_FEATURE_ENABLED === 'true') { const featureModule = await import('./feature-module.js'); // ... use featureModule }
Conditional client-side routes
We often wish to develop 2 highly related SPA apps for say "normal" users and "admin" users. Admins need all the client side routes that normal users have + the admin feature routes. Because these two app share many features, it's convenient to build all the features and their client routes for both app using a single TypeScript project using an SPA framework like React or Vue. However, we'd like the lite non-admin app to load faster without the admin level baggage and there are security benefits of not exposing the admin code to all users. Using Vite, I can build the client side routes for each app using
if (import.meta.env.VITE_ADMIN_APP)conditions and then let Vite's conditional bundling, tree shaking and dynamic loading do the rest with no TypeScript compilers or language servers being harmed in the process.I posted an article about this that includes a demo app.
Reacted by Denis Migdal and Ricardo Fernández SerrataConditional compilation with Terser :
if( __DEBUG__ ) { console.warn("debug"); } console.warn("Hello");
$ terser main.js -d __DEBUG__=false console.warn("Hello");
You can also use it for functions :
function __LOG__(m) { if(__DEBUG__) console.warn(m); } __LOG__("ok");
$ terser main.js --toplevel -d __DEBUG__=false
You can also specify nested constants:
--define env.DEBUG=false.Of course there is a lot of options, and a node API.
Reacted by Ryan Cavanaugh and Ricardo Fernández SerrataThere's also unplugin-preprocessor-directives that can be used in Vite, Rollup, Webpack, Nuxt and esbuild to get build-time preprocessing. It supports:
#if/#elif/#else/#endifconditions via comments.
On codeplex this was a popular feature request:
https://typescript.codeplex.com/workitem/111
https://typescript.codeplex.com/workitem/1926
Personally I think preprocessor directives like #if, #elif, #else #endif with possibility to specify symbol to compiler would be very useful.
And a way to mark function (or ambient function declaration) as Conditional (something like ConditionalAattribute in C#) would be great improvement too.
I have lot's of
console.loglike function calls that decrease performance of my application and ability to easily remove all calls to this function would be great.