Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support conditional compilation #449

Open
mpawelski opened this issue Aug 13, 2014 · 91 comments
Open

Support conditional compilation #449

mpawelski opened this issue Aug 13, 2014 · 91 comments
Labels
Needs More Info The issue still hasn't been fully clarified Suggestion An idea for TypeScript

Comments

@mpawelski
Copy link

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.log like function calls that decrease performance of my application and ability to easily remove all calls to this function would be great.

@RyanCavanaugh
Copy link
Member

We'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.log case, 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).

@zpdDG4gta8XKpMCd
Copy link

I 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.

@NoelAbrahams
Copy link

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();

@CyrusNajmabadi
Copy link
Contributor

Having 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.

       -- Cyrus

From: 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.

@mpawelski
Copy link
Author

I 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 readable

Conditional 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.

@RyanCavanaugh
Copy link
Member

To 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

@mpawelski
Copy link
Author

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);

@s-panferov
Copy link

I 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:

  1. 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).
  2. 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?

@paul-reilly
Copy link

@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.

@zpdDG4gta8XKpMCd
Copy link

@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 declare const debug = false which gives the runtime a strong hint to optimise things like if (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:

@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)
.

@fletchsod-developer
Copy link

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).

@jez9999
Copy link

jez9999 commented Jan 20, 2015

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.

@agnauck
Copy link

agnauck commented Apr 3, 2015

for me it would be very useful for:

  1. targeting different platforms, like browser vs node
  2. 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.

@fletchsod-developer
Copy link

At my company, we're discourage from using any compilation option in source code (cuz it deals with CPU) and we're required to use the config file (App.Config, Web.Config, *.Config.Debug, *.Config.Release) instead. So, it's a moot point here when it come to some business and not personal preferences.

@AbubakerB
Copy link
Contributor

Is there any update on this? I know its kind of a pain to implement . . . but its also very useful for us typescriptters :) .

@mhegazy
Copy link
Contributor

mhegazy commented May 1, 2015

We need a proposal for this feature. Pre-processor directives (i.e.#ifdefs) are not desirable. But something along the lines of Conditional in C# would be more like it. Either ways we need a proposal for this to move it to the next stage (i.e Accepting PRs).

@tinganho
Copy link
Contributor

Right now, this is a deal breaker for me in a project I'm working on.

I helped developed an in-house isomorphic framework that could be ran on server and the client. So in AMD we use a lot of conditional imports and conditional executions.

defined(function(exports, require) {
  if (insServer) {
    // require and do something
    // do something
  }
  else if (inClient) {
    // require and do something
    // do something
  }
  // do common stuff
});

We haven't been able to switch to TS because there are no mechanism that deal with our problem.

I think there many other projects that could benefit from this. At least many cross platform frameworks need this feature.

@tinganho
Copy link
Contributor

I'm also not sure if the conditional in C# would fit our use case.

@basarat
Copy link
Contributor

basarat commented Jul 16, 2015

We haven't been able to switch to TS because there are no mechanism that deal with our problem.

as a workaround why not have a different ambient .js file for server vs. client

@karldodd
Copy link

In the msdn blog post Angular 2: Built on TypeScript:

We have worked with the Angular team to design a set of new features that will help you develop cleaner code when working with dynamic libraries like Angular 2, including a new way to annotate class declarations with metadata. Library and application developers can use these metadata annotations to cleanly separate code from information about the code, such as configuration information or conditional compilation checks.

So, isn't Angular 2 a use case? :)

Also I'd like to provide my use case: we need to compile different versions of our js library. Some versions implement a feature in a powerful complex way, while some versions implement the feature in a very simple way (size is smaller and size matters).

@jameskeane
Copy link
Contributor

@Aleksey-Bykov I agree with you here.

Ideally I'd like to see something like the closure compiler, where you can provide compile time definitions:
closure-compiler --define "DEBUG=true"

@CyrusNajmabadi
Copy link
Contributor

CyrusNajmabadi commented Oct 30, 2022

The preprocessor needs to know the grammar as these can appear in complex syntactic constructs in typescript like inside a template literal hole.

Typescript cannot be context free lexed. It's lexing must be driven by a parser.

You are assuming the languages fundamentally work the same. They do not.

Specifically, the pp needs to know it can process #if ... #endif in:

let bar = 0;
let baz = 1;
let v = `foo ${
#if false
bar
#else
baz
#endif
} quux`;

But that it can't strip anything in:

let v = `foo {
#if false
bar
#else
baz
#endif
} quux`;

In the first piece of code you have a template literal that contains an interpolation hole (starts with ${. As such, you are now out of the string and the pp can be involved. In the second, you are in the template literal entirely. The second is legal code today and cannot change meaning. As such, the pp needs to understand the lexical and syntactic grammars of typescript to understand which of these it participates in and which it does not.

@alenl
Copy link

alenl commented Oct 30, 2022

You know what's funny, if you just take the markdown you wrote and tag it correctly, you will see that even the GitHub's syntax highlighter for markdown can correctly distinguish those:

let bar = 0;
let baz = 1;
let v = `foo ${
#if false
bar
#else
baz
#endif
} quux`;
let v = `foo {
#if false
bar
#else
baz
#endif
} quux`;

So yeah... it's super complicated. 🙂

The lexer grammar is based on this IIUC, but I'm sure you can write it in your sleep if you wanted to accept that the problem merits a solution. If you don't, just say so - no need to pretend it's too hard. 🙂

@CyrusNajmabadi
Copy link
Contributor

As i said, conditional compilation is one of the most complex things i've seen in every compiler i've worked on. It is probably not as simple as one would like :)

no need to pretend it's too hard.

I didn't say it was too hard. I said it was complex. That remains very true.

You know what's funny, if you just take the markdown you wrote and tag it correctly, you will see that even the GitHub's syntax highlighter for markdown can correctly distinguish those:

I didn't say it couldn't be distinguished. I was pointing out why one cannot just drop in a C/C++ preprocessor and have it work properly for TS/JS. I was also pointing out that trying to do that would lead to breaking changes which would not be acceptable.

@CyrusNajmabadi
Copy link
Contributor

if you wanted to accept that the problem merits a solution. If you don't, just say so

Please stop trying to infer positions from me that i am not stating. I've made my posts clear and specific about what they're saying. The continued effort to try to draw out additional arguments from that that i'm not making it not helpful.

@MarcWeber
Copy link

On TS side the implementation is as complicated as providing a hook processing
code after it has been loaded from disk so that some parts can be dropped.

like

const lines = file.split("\n")
find all lines starting with #if (.*) #else #end .. 
eval the (.*) condition.. then replace the lines between #if and #else
with "" 

Of course could be made more complex like haxe and others.
But it could be done wihin less than 15 lines of code I guess.

On browser side its equally simple.

Now how abuot packaging ? If you drop packaging (cause TS does pakcage anyway)
the problem is gone. Otherweise the if the conditions get evalutaed in its own
scope the scope inputs (eg flags) could be used to prefix the dist files

dist/+client/...
dist/+server/...

However amount of cases might explose once you add if x < 1.0 & 1.1 ..

But the question is a different one: If JS had specs TS would follow.
TS by definition follows JS. So I see 3 solutions

  1. change the focus of TS
  2. fork TS
  3. move this wish to JS world and wait till TS follows

The funny thing is that JS doesn't have the typing issue. Cause its not typed.

I mean if client / server code has different types JS will not mind.

So its more a 'typing issue' rather than a JS issue ?

No doubt TS makes life easier in many cases.

But the TS idea can be applied to more languages.

So if you're interested in starting the latter drop me an mail.

Cause TypeScript has raised the bar IMHO.

And esbuild /next.js rewriting babble or such using rust show
that JS is not the best tool for speed in some cases (?).

So there are questions on other levels, too.

@tohagan
Copy link

tohagan commented Oct 31, 2022

Thanks for this more in depth explanation of the complexity of implementing an IDE language server. I think you've convinced me that there should be a better way.

While I'll argue the case for a pre-processor if it's workable, I'm equally supportive of alternatives that achieve the same result.

So I'm going to suggest that we might backup a level and rethink the solution. I'm all for simpler!

Does the existing TypeScript compiler perform compiler optimizations like

  • Compile time evaluation of constant expressions?
  • Dead code elimination for "if" statements (i.e. when the condition is a constant expression)?

If so, then we may already have conditional compilation!
If not, then how far off would that be from being a viable solution?

(Post: OK, I now see that listed under TS Non-goals is "Aggressively optimize the runtime performance of programs. Instead, emit idiomatic JavaScript code that plays well with the performance characteristics of runtime platforms." So I'm guessing the answer is NO to the optimizations being already done. I wonder if this feature request would be sufficient argument for reconsidering these as potential code reduction optimizations?)

@CyrusNajmabadi
Copy link
Contributor

like
const lines = file.split("\n")
find all lines starting with #if (.*) #else #end ..

No. This would not work. I even gave an example above of how this would break existing legal typescript code.

Specifically, it would change the meaning of this code:

let v = `foo {
#if false
bar
#else
baz
#endif
} quux`;

Furthermore, this would not work with any higher layers as now even basic things like positions and file contents would be wrong. Take something as basic as formatting. You can't make edits safely if your positions in the file are incorrect or you have an indirect understanding of what text exists between any tokens.

@alenl
Copy link

alenl commented Nov 1, 2022

Please stop trying to infer positions from me that i am not stating

Ok, I apologize for misinterpreting then. Can you please clarify if I understood correctly now. It sounds to me like we all agree on this:

  1. This is a feature that would be useful to some users of the language.
  2. If it is to ever be added, it must not change the meaning of existing/unaware valid programs (e.g suddenly start interpreting contents of the strings).

Is that correct?

And, you also say that this would be very hard to add to the compiler, right?

@CyrusNajmabadi
Copy link
Contributor

CyrusNajmabadi commented Nov 1, 2022 via email

@NumbGnat
Copy link

NumbGnat commented Nov 1, 2022

I want to understand something... If left alone, how is the following valid code?

let v = `foo {
#if false
bar
#else
baz
#endif
} quux`;

Calling v(); would return an error, because #if false isn't valid TypeScript. Correct?

If that's the case, it seems to me that if the developer created that code, they wanted it to be conditionally compiled.

Nevermind that the #if false condition would never be included...

But maybe the developer did that on purpose because bar was causing problems, so they wrote baz as a temporary workaround until they could get in and properly debug bar. 🤔

@CyrusNajmabadi
Copy link
Contributor

CyrusNajmabadi commented Nov 1, 2022 via email

@NumbGnat
Copy link

NumbGnat commented Nov 3, 2022

how is the following valid code?

It's simply a multi line template string literal. This has been supported in JavaScript and typescript for years.

Sorry... At first glance, it looked like a function definition. 🤦‍♂️

@CyrusNajmabadi
Copy link
Contributor

Nothing to be sorry about :)

@tohagan
Copy link

tohagan commented Nov 4, 2022

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 falsey not output the if statement body.

@dyst5422
Copy link

dyst5422 commented Nov 5, 2022

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 falsey not 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

@c0nf1gur4t0r
Copy link

This is a feature that would be useful to some users of the language.
Yes. Agreed.
If it is to ever be added, it must not change the meaning of
existing/unaware valid programs (e.g suddenly start interpreting contents of the strings). Right. Breaking changes are bad.
And, you also say that this would be very hard to add to the compiler,
righ I'd put it in the very-complex/costly category. It is both challenging to add and it impacts practically everything above. Given that, it has to be massively useful to a huge part of the ecosystem to justify its costs.

On Mon, Oct 31, 2022, 5:21 PM alenl @.> wrote: Please stop trying to infer positions from me that i am not stating Ok, I apologize for misinterpreting then. Can you please clarify if I understood correctly now. It sounds to me like we all agree on this: 1. This is a feature that would be useful to some users of the language. 2. If it is to ever be added, it must not change the meaning of existing/unaware valid programs (e.g suddenly start interpreting contents of the strings). Is that correct? And, you also say that this would be very hard to add to the compiler, right? — Reply to this email directly, view it on GitHub <#449 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABC2MY4VGSPD6YSEWUM7DMTWGBPBPANCNFSM4AS4QOEQ . You are receiving this because you commented. Message ID: @.>

It's so hard to add a rule validation on tsc BTW configured in .tsconfig? It would be something like C# compilation process with Debug and Release rules IMHO

@CyrusNajmabadi
Copy link
Contributor

CyrusNajmabadi commented Nov 5, 2022 via email

@tohagan
Copy link

tohagan commented Nov 12, 2022

My example I think has skewed and limited the scope of what I meant so let me try to clarify.
Firstly, I'm not limiting this to a Realase / Debug build switch that's just one specific case.

More Examples:

const PLATFORM = "Windows";
const LOGLEVEL = 2;

// if statement is entirely removed since PLATFORM === "Linux" is always false
if (PLATFORM === "Linux")  { 
  dostuff1();
} 

// condition is always true so if statement is removed and we emit just:  { dostuff2(); }
if (LOGLEVEL <= 3) { 
  dostuff2();  
}  

// condition is always true so if statement is replaced with just: { dostuff3(); }
if (LOGLEVEL <= 3 && PLATFORM === "Windows") {
  dostuff3();
} else {
  dostuff4();
} 

// replace with isWindows = true;
let isWindows = PLATFORM === "Windows";

// condition is always true so replace with: const msg = "Yes"  
const msg = LOGLEVEL <= 3 && PLATFORM === "Windows" ? "Yes" : "No";

// msg condition is always false remove the if statement
if (msg === "No") { 
  dostuff5();  
}

// replace with { dostuff6(); }
switch (PLATFORM) {
  case "Windows": 
    doestuff6();
    break;
  case "Linux": 
    doestuff7();
    break;
  default: 
   doestuff8();
}

A compiler optimiser traverses the AST tree and identifies sub expressions that contain constants (litererals and const variables) and converts them to a single value at compile time.

For an if statement where the resulting condition expression is reduced at compile time to be a falsey or truthy constant, the if statement and either the then or else statement block can be removed (this is called "dead code elimination"). Note that "dead code elimination" can be applied to other conditional statements such as a for loop but that's a discussion for another day :) . To cover the case where a then or else block declares local variables, you'd need preserve that block scope by keeping the { ... } curly brackets .

This essentially gets us #if / #elif / #else like behavior without introducing any syntax or semantic changes. There is another benefit to this approach over a pre-processing solution in that the TCS compiler will still perform syntax and type checks for code that would be removed. This is very important for code maintenance.

As has been pointed out, in JS/TS you can break the rules and force update a const value. So in the interests of backward compatability, constant expression reduction would need to be a feature you enable via a compiler option in tsconfig.json which implicily indicates to the compiler that you're not breaking these rules. Of course it will only be appled to transpiled code in the current project so won't alter any imported modules.

Finally, to make good use this feature, you need a conveniently way to set one or more constants at compile time without altering the code so you can script different build types that tweak these constants. This is commonly done in C/C++/C# land as a commmand line option. Example:

$ tsc -D 'PLATFORM="Windows"'

So this would mean the compiler would pre-declare these constants before compiling the each source file.

You're likely to also want to be able to set a default value for these constants so the command line compiler options just overrides changes to these defaults. One logical place for setting these default values I think would be in tsconfig.json although another way to do this would be have a command line option to includes/exclude specific TS source files that declare and set these constants. This would allow an IDE to assist in checking these source files.

@tohagan
Copy link

tohagan commented Nov 12, 2022

So if you go down the compiler optimisation route ... What do you loose over a pre-processing solution? I can think of a few things. As always from me ... these are just "brainstorming ideas" not "opinions" 😉 .

Firstly, you can't easily do conditional imports. Placing an import inside an if statement would isolate those imports to that if statement's block scope which is typically contrary to our import goal. This is disappointing because conditional importing is one of the most beneficial features in terms of reducing emitted code size and for creating build time variations in code. This is particluarly helpful for apps to reduce unused platform or browser specific code from a build. A common use case is where we wish to choose to import different implementations of an interface based on a build option.

We hit the same issue with conditional declarations of variables and functions inside if statements as their declaration is block scoped. Example:

if (cond) {
   const a = 1
   function X() { .... } 
} else {
  const a = 2
  function X() { ... }
}

... is not the same as ...

#if cond
   const a = 1
   function X() { .... } 
#else
  const a = 2
  function X() { ... }
#endif

... since in the 1st example our function declaration is restricted to the scope of the if/else block.

A related missing feature would be macros that remove code. An example of this in C/C++ is assert(cond) that is conditionally declared either as a macro that checks and throws exception if the condition is false OR is removed entirely when _DEBUG is 0 / false.

#if DEBUG
#define assert(cond) if (!(cond)) throw new AssertException(##cond) 
#else 
#define assert(cond) 

To fix this, you'd need some kind of compiler optimisation to say ... remove calls to empty functions ... but again you've got to solve the same nested block declaration problem.

So where does that take us? I think this little mental exercise suggests that without a pre-processor we're lacking the language primitive to be able to conditionally modify declarations in the current block scope and that while optimising the standard if or switch statement is still very useful, it won't fully solve this more general problem. The conditional import' statement suffers the same issue. So if we want the full expressive power that a C/C++/C# like pre-processor delivers, we'd need to declare a new kind of iforswitchstatement (let's call it~ifor~switch) that does not create a new nested block scope but instead can delare new symbols that are added to same scope as iforswitchstatement in the same way that#ifpre-processing does. These new TS statements would emit JS code that declares symbols outside the~ifor~switchand then uses code inside to set them. The emitted code would be simplified when these~ifor~switch` statements are optimised to remove the condition and replace the nested block.

For TS, this even might mean the compiler would require all code paths in an ~if or ~switch to declare the same symbols with matching types, otherwise we will need to union the declared types from all execution paths to be able to type check the code that follows these statements ... if you cared aboout this.

@ANFADEV
Copy link

ANFADEV commented Dec 20, 2022

Hi everyone,

If anyone is interested i made Prebuilder

Currently works best with rollup, but for typescript projects it would need an adapter
(which i made but needs .js config files, which are still not there in Typescript)

But it can be easily hoked up to your projects manually anyway, once you take time to configure source/output folders.
I personally use it for my projects 👍

@tohagan
Copy link

tohagan commented Dec 24, 2022

Well done @ANFADEV !

@TheDirigible
Copy link

Use-case: catching errors before run-time. IMHO the main purpose of Typescript.

Take this error in code shared between client and server builds:

#if Server
    Client.doThing()
#endif

At edit-time, both client and server are considered available, so this would not error.
This error should be caught at compile-time. Without conditional compilation, we must use hacks that result in errors like this making it all the way to run-time.

@tohagan
Copy link

tohagan commented Jan 19, 2023

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 falsey not 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.

@zm-cttae-archive
Copy link

zm-cttae-archive commented Feb 3, 2023

I agree with this comment - #449 (comment) - I want to build cross-environment packages that leverage browser, node, etc native APIs in a sane manner.

An example use case is a package that runs advanced text parsing operations without pulling in a bazillion NPM packages (so: TextDecoder and TextEncoder APIs, Node's Crypto.Hash vs Web's CryptoSubtle; Fetch from node-fetch vs Web built-in; and other features).

We would need both optionalLibs and optionalTypes properties in compilerOptions to handle these undue complexities so that we don't have to track global properties and put incompatible definitions into compiler.types and compiler.libs:

  • Any instances of declare would create optional ( | undefined) properties in Global object.
  • To make things simpler, interfaces would be available via direct import type.
  • Type narrowing would be used to be sure that the global object actually has the property and is using that env.

@JessicaMulein
Copy link

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?

@MarcWeber
Copy link

MarcWeber commented Jun 14, 2023 via email

@zm-cttae
Copy link

@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.

@qwertie
Copy link

qwertie commented Jan 11, 2024

Oh god, please not #if. It's a trifecta of awfulness:

  1. it tends to be very limited (e.g. in C, you cannot write conditions that refer to the program's types, functions or variables)
  2. the engineering effort to implement it (including syntax highlighting in VS Code etc) would be high
  3. the syntax/semantics of #if are 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 production

Here 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 the if type block (its braces are deleted)
  • There are intrinsics like compilerOptions (from tsconfig.json) and dependencies (from package.json)
  • tryGet<T, Alt=unknown> is a magic error-suppression intrinsic that returns Alt if symbol T cannot be found
  • X extends Y means X extends Y ? true : false (this need not be limited to the if type context)
  • if type (X) means if type (X extends (false | 0 | null | undefined | '')) except that it shows an error if X is never or unknown or ambiguous (both truthy and falsy, e.g. any or boolean or a generic type T).

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 X is not defined?"

if type (X) {
    function foo() {}
}
type X = true;

And what if X is 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 < 2 because this kind of feature would make people try stuff like that.

@tohagan
Copy link

tohagan commented Jan 11, 2024

We'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.

@qwertie
Copy link

qwertie commented Jan 11, 2024

No, if type is a fundamentally new construct so there's no backward compatibility to break. (But if it would make you feel better, it could be called #if without taking on any of the other syntactic or semantic baggage from C/C++)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs More Info The issue still hasn't been fully clarified Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests