ts-loader icon indicating copy to clipboard operation
ts-loader copied to clipboard

Enum values from d.ts files cause exception when build with transpileOnly=true

Open ArtemZag opened this issue 7 years ago • 23 comments

I tried to speed up build process. And transpileOnly=true works amazing! Until I use any enum value in my code.

Example: Definition file: definitions.d.ts

declare module MyModule {
  export const enum MyEnum {
    EnumValue1 = 0,
    EnumValue2 = 1
  }
}

TS file: example.ts

/// <reference path="./definitions.d.ts" />
console.log(MyModule.MyEnum.EnumValue1);

In runtime I get error: "MyModule is not defined" Is there any way to force ts-loader NOT to ignore some specific *.d.ts files when I use transpileOnly=true? (something like transpileOnlyExcept...)

ArtemZag avatar Oct 20 '16 20:10 ArtemZag

Any news on this one?

wub avatar Jul 04 '17 02:07 wub

We don't have a way to reproduce this issue and there's nothing we can look into at present...

johnnyreilly avatar Jul 04 '17 04:07 johnnyreilly

@johnnyreilly I just stumbled across this issue today, as we have just started using the transpileOnly option. In my case, we are using regular ES6 modules (imports and exports).

I have prepared a repro repo, please have a look: https://github.com/schmuli/ts-loader-enum

As I mention in the readme, I noticed that typescript will also throw an error when using isolatedModules, is this the same or a similar issue for ts-loader?

schmuli avatar Sep 26 '17 14:09 schmuli

I think this comes down to transpileOnly not producing .d.ts files. As such it's probably expected behaviour. There's a good explanation here: https://github.com/Realytics/fork-ts-checker-webpack-plugin/issues/49

There's also a suggested solution mentioned by @piotr-oles. All we need is someone to implement it :wink:

FWIW I suspect it makes more sense for this to be implemented in ts-loader rather than the fork checker. Up until now the fork-ts-checker-webpack-plugin hasn't emitted anything whereas ts-loader has. I can imagine having an option that emits .d.ts when transpileMode is true

johnnyreilly avatar Sep 26 '17 14:09 johnnyreilly

I thought it was the .d.ts file, so I changed the file to a regular .ts file. However, now the const enum is still not being removed, and is included in the webpack output.

Again, this is probably a result of using transpileOnly, which is similar to isolatedModules. I will have a look in the TypeScript issues.

schmuli avatar Sep 26 '17 14:09 schmuli

Here is an (open) issue from TypeScript: https://github.com/Microsoft/TypeScript/issues/16671.

And another comment on isolatedModules: https://github.com/Microsoft/TypeScript/issues/10879#issuecomment-248077719

schmuli avatar Sep 26 '17 15:09 schmuli

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Jan 19 '19 12:01 stale[bot]

Currently have the same issue. Commenting for this not to be closed

dark-monst avatar Feb 03 '19 14:02 dark-monst

Same as @dark-monst In my case, I created a definition .d.ts file to override a module's function definition. It picked up w/o any issues on my IDE, same definition file does not get picked up at compile time :(

fforres avatar Mar 04 '19 22:03 fforres

@schmuli repo is spot on. Experiencing all same issues here, including using the const enum in a .ts file

cortopy avatar Aug 12 '19 18:08 cortopy

Quickly reading the source code I can see that transpileOnly uses what looks like an isolatedModule api

I'm guessing that transpileOnly overrides whatever configuration there is for isolatedModules in tsconfig. Would there be a way to change this? It would probably solve this issue

cortopy avatar Aug 12 '19 18:08 cortopy

I think you can control isolatedModules via tsconfig.json:

https://github.com/microsoft/TypeScript/issues/16351

There's also some useful content here: https://www.typescriptlang.org/docs/handbook/compiler-options.html

johnnyreilly avatar Aug 12 '19 18:08 johnnyreilly

Indeed you can, but that compiler flag has no effect in ts-loader. It's overriden by using the isolated module ts api whenever transpileOnly is on (or so it seems to me)

cortopy avatar Aug 12 '19 19:08 cortopy

https://github.com/TypeStrong/ts-loader/search?utf8=%E2%9C%93&q=isolatedModules&type=

Are you sure? Doesn't look like ts-loader is doing anything in that space...

johnnyreilly avatar Aug 12 '19 19:08 johnnyreilly

I'm not sure at all!!! But I've tried disabling isolatedModules and ts-loader still inlines the enum. I've not digged into the source code, but I was looking at different api usage and "guessing" this may be why.

Looking again, I can see in here that ts-loader doesn't indeed override the compiler flags when transpileOnly is on.

However, in my mind, it's a process of elimination. const enum are properly removed using either tsc or transpileOnly: false. It seems to me that because this option leads to different usage of the compiler's api, here may lie a possible solution. Unless the fault lies with the compiler itself when using the transpileModule method....

I'm not really that familiar with the compiler's api sorry :blush:

cortopy avatar Aug 12 '19 19:08 cortopy

Just got the same problem. Read the whole issue, but haven't got the idea of why it happens. What's wrong with transpiling .d.ts the same way as it's done when transpileOnly disabled? Could you explain the idea behind that? :)

lancedikson avatar Aug 22 '19 12:08 lancedikson

any updates?

manemao avatar Sep 22 '19 14:09 manemao

Ran into the same issue, any solution? Would love to be able to use transpileOnly since its so much faster.

nsgundy avatar Oct 28 '19 12:10 nsgundy

any updates on this ? @johnnyreilly

await-ovo avatar Apr 03 '20 09:04 await-ovo

FWIW I ran into this same issue but with a .d.ts file that didn't have any enums in it.

It was a typedef for a module that had a broken typedef, that I wanted to fix as per (https://stackoverflow.com/a/41641001/103395). I first had it in modulename.d.ts and then ts-loader gave me this error.

Then, when I did the suggested change more literally, i.e. I made a types-overrides folder, added that to paths in my tsconfig, and renamed the typedef to types-overrides/modulename/index.d.ts, and then suddenly it did work. I don't understand why, it's the exact same contents and transpileOnly has been on all the time.

Sharing this just in case other people find this issue with non-enum-related causes. Maybe messing around with where your .d.ts is and what it's named helps you as well.

eteeselink avatar May 09 '20 16:05 eteeselink

I've been taking a look at this thread as quite a few people still seem to be coming up against issues around this. I see several different issues mentioned here:

  • Why do const enums exported from .ts files not get removed from the code?
  • Why do const enums exported from d.ts files cause an error when transpileOnly is true, but not otherwise?
  • Why can't you set isolatedModules to false with transpileOnly set to true? Can this be changed?

I investigated what is happening in ts-loader, typescript and webpack and have detailed my thoughts below. The summary is:

  • In transpileOnly mode exported const enums will always generate an inline function in the emitted code as if they were not const.
  • Because the exported const enum is causing code to be emitted it needs to be in a .ts file, not a .d.ts file.
  • With transpileOnly set to true isolatedModules will always be set to true, whatever settings you make in tsconfig.json or ts-loader options.
  • This behaviour is fundamental to the way transpileOnly works. We cannot change the way const enums are handled and still get the speed benefits of transpileOnly.

I assume some people may be disappointed that this is not a bug in ts-loader which can be fixed, or that isolatedModules cannot simply be switched off. transpileOnly is a great feature which greatly reduces compile time in large projects but it is mutually exclusive with exported const enums.

I hope that with this understanding people will be able to adapt their code to be able to use transpileOnly and enums. The fact that the compiler allows const enums but converts them to functions may be surprising but it means large codebases with many const enums can be compiled using transpileOnly. It does mean there is a subtle difference between the code generated normally and with transpileOnly.

If you must use const enums and cannot accept them being included in the output bundle I suggest turning off transpileOnly and using project references for that part of the code. Project references allow a part of the codebase to be pre-compiled. In this way, you only need to compile it once so that the time penalty of compiling without transpileOnly is less relevant.

Here is what I found to reach these conclusions:

When transpileOnly is set to true ts-loader uses typescript's transpileModule function to perform the transpilation. The code for transpileModule forces several compiler options to undefined or fixed values:

https://github.com/microsoft/TypeScript/blob/4fe27222ca2d012dad541b43d791ae12f1cf6985/src/services/transpile.ts#L40

Option values are forced if they have a transpileOptionValue field, which for isolatedModules is true:

https://github.com/microsoft/TypeScript/blob/b977f86abd2c2f67ccdc0e61b0ac264a01d1e27c/src/compiler/commandLineParser.ts#L501

So no setting in ts-loader will override isolatedModules when transpileOnly is true. This is not something which can, or should be, fixed. Without transpileOnly the compiler needs to open up every file being imported to check the types so it can see if there is an error. In transpileOnly mode it just looks at the one file it is working on and changes it from typescript to plain js. This is why transpileOnly is so fast but it also means the compiler cannot use information from other files.

When you put a const enum in an ambient declaration file this is fine when not using transpileOnly. The compiler knows about the enum because it has read the declaration file, so it can substitute the value of the enum fields in the file it is compiling. In transpileOnly mode this is not possible. We would not want to change this behaviour otherwise the compiler would need to check all files which might contain information relevant to the file it is compiling, which would essentially disable transpileOnly mode.

I looked at the repo kindly provided by @schmuli: https://github.com/schmuli/ts-loader-enum. Here, the const enum is in common.d.ts, which is imported in src/main.ts.

With transpileOnly disabled, the compiler reads common.d.ts when it compiles src/main.ts. It recognises that it only contains code for typescipt and will not emit any js code, so it uses the information when compiling src/main.ts and does not include an import statement to import common in the final code.

When transpileOnly is true, the compiler does not readcommon.d.ts but assumes that it refers to a common.js file which will be available when bundling so it includes an import statement in the emitted code. When Webpack tries to bundle the code it cannot find common.js, which is the error it reports.

When you include a statement such as

import { File, FileType } from './common';

the compiler will check for the existence of common.d.ts but if it finds it then it expects there will be a common.js in the final output. The d.ts file is just there to tell the compiler what is in the .js file so that it can do type checking. If you have a d.ts file which is not coupled with a .js file (such as a file which just contains interface definitions ) I believe it would be normal to list that file in the "include" section of tsconfig.json rather than to import it.

The error can be fixed, as @schmuli noted, by changing the .d.ts to a .ts file, but at the expense of having the const enums inlined as if they were not const. The functionality is the same but there is more code emitted. In most cases this will be fine but there may be cases where the enum function increases the bundle size unacceptably.

When using transpileOnly, exported const enums are always emitted as an inline function and not removed as you might expect a const enum to be. If you take a look at shouldEmitEnumDeclaration in the link below you can see that the enum code will be emitted if either preserveConstEnums or isolated modules is true:

https://github.com/microsoft/TypeScript/blob/eb3645f16b66ed05110ea957c77773cea739bae8/src/compiler/transformers/ts.ts#L2311

This makes sense because with isolatedModules the compiler is only looking at one file at a time. It cannot open the file the const enum is defined in to use that definition to remove the enum. So exporting a const enum will never work with isolatedModules, which is why the compiler automatically exports it as an inline function. This means the compiler generates code when you might not expect it to, leading to the issue above.

The issue is also discussed at the link below:

https://github.com/microsoft/TypeScript/issues/16671#issuecomment-344449974

transpileOnly means transpile every file one at a time.. when the compiler is looking at one file, it has no way to know if the reference it is looking at is a const enum or not, since the declaration is in another file that it does not have access to.

so I do not think you can mix these two concepts, const enums (require whole program information), and transpileOnly (one file at a time).

To be clear, I believe you can use const enums and transpileOnly, but if you export the const enums they will be converted to normal enums and included in the output bundle.

appzuka avatar Jun 21 '20 19:06 appzuka

Thanks for a really clear and helpful write up Nick!

johnnyreilly avatar Jun 21 '20 21:06 johnnyreilly

when you use transpileOnly:true you can try this plugin https://www.npmjs.com/package/babel-plugin-typescript-hardcoded-enum

xiaopingbuxiao avatar Sep 09 '22 08:09 xiaopingbuxiao