Open staeke opened 7 years ago
MyInterface
should never be written to the generated .js. instead the compiler will use Object
. emitDecoratorMetaData
is a global switch, and adding it tells the compiler to try to write the metadata for decorated declarations. flagging these as errors means you can not decorate a method/property thorough out your code base unless it is of a class type.
Thanks for the reply @mhegazy. I understand that's the way it works. My hope was that I outlined such a reasonable case that either you'd
Wouldn't it be much better to just not emit metadata for interface types then?
As @mhegazy stated, this would preclude decorating any members or member parameters that are not of class types.
As metadata emit is an optional and secondary consequence of the presence of a decorator in the source. Annotating all such members with meaningfully metadata emittable types would be prohibitively cumbersome and the resulting code would of far poorer quality in substantial number of cases.
There are a substantial number of cases where it is desirable to have metadata enabled but still decorate members they will not result in meaningful metadata but for which the behavior of the decorator is meaningful.
For example
import {autoinject, bindable} from 'aurelia-framework';
import {Router} from 'aurelia-router';
@autoinject export default class {
constructor(readonly router: Router) {}
@bindable model: {name?: string} = {};
}
In the above example, I need the metadata for router
, but not model
. However its presence on the latter is not harmful but if I were required to use a class for model
I would simply stop using decorators as doing so would result in a less maintainable, less idiomatic program in this case
Wouldn't it be much better to just not emit metadata for interface types then?
we have no way of doing so at the moment. the emitMetaData uses the runtime value for serialization, this works for classes/enums/primitives, but not for interfaces. interfaces do not exist at run time. Emitting interfaces means we have to either materialize interfaces as objects at runtime, or have some form of type serialization at runtime. you can find related discussion in https://github.com/Microsoft/TypeScript/issues/3628
Somehow I wasn't clear enough when describing my problem, which had to do with the wrong decorator reference being emitted. However, upon further investigation I actually realized that this is a problem with the webpack ts-loader plugin, and not the actual typescript compiler. So my apologies. I've raised the issue there https://github.com/TypeStrong/ts-loader/issues/613
My bad again. This really seems to be an issue with Typescript. I opened an issue with ts-loader here https://github.com/TypeStrong/ts-loader/issues/613#issuecomment-325309872 only to realize that they're just using the Typescript language service. So with my recent findings, let me summarize:
ts-loader yields different results when running these calls (the option transpileOnly
in ts-loader determines which one to run)
languageService.getEmitOutput(filePath, ...)
compiler.transpileModule(fileContents, ...)
The first call generates
__decorate([
Input(),
__metadata("design:type", Object)
], MyClass.prototype, "myi", void 0);
whereas the second generates
import { MyInterface } from './my-interface'
//...
__decorate([
Input(),
__metadata("design:type", typeof (_a = typeof MyInterface !== "undefined" && MyInterface) === "function" && _a || Object)
], MyClass.prototype, "myi", void 0);
As you can see, the second one references MyInterface
, which, again, is just a compile time construct. It seems to me that transpileModule
should yield the same results.
But of course, I realize that in order to know that it's just an interface you'd have to keep track of all the files. So maybe it's a big change to support. Maybe transpileModule
could be part of the language service but now I'm a little bit out there.
The difference in emit is intentional. in single-fle-transpile mode (i.e transpileModule
) the compiler has no way to know if the name is an interface or not. the emit is a runtime check for what the compiler does at design time if had the full program in memory. it should be functionally equivalent though in the case of interfaces.
First, let me just state that I'm really grateful for your work and that this may come off as fractious. I'm really not - just want Typescript to become as good as it can (and solve my problems of course :))
I understand that single-file transpile means that this error occurs. And that's how it's implemented. But it seems to me this case is an argument for that single-file-transpile is a bad option here. The way I see it, it only becomes an error if:
require(...).MyInterface
is ok, albeit undefined
)...i.e. not the ordinary use case, but not really esoteric either.
So given that this is the case, moving forward - what would we want it to be? I can see the following options. What is your stance/comment on these?
dontTypeCheck
(or similar) option to the getEmitOutput
APIs. After all, probably the biggest use case for transpileOnly option is speeding up transpiling in day-to-day workflows. With this option the compiler would replace interfaces with objects in decoration, and possibly other things required for a valid transpilation, but not any other type checking.Transpile only is improved by keeping some kind of file cache of what is what (and more). This would most likely change the APIs
that is not possible. the compiler does not have a way to examine all files, and there is no guarantee it will ever see all files in a single transpilation.
Add some dontTypeCheck (or similar) option to the getEmitOutput APIs. After all, probably the biggest use case for transpileOnly option is speeding up transpiling in day-to-day workflows. With this option the compiler would replace interfaces with objects in decoration, and possibly other things required for a valid transpilation, but not any other type checking.
Not sure how that solves the issue. we still have the single-file-transpile mode.
I think there is a bug here that we should address. the output currentlly is:
import { MyInterface } from './my-interface'
which is invalid ES6 import. the correct thing to do is to have an import to the module instead, i.e.
import * as MyInterface from './my-interface'
and then use MyInterface .MyInterface
to check if the interface is undefined
.
That indeed sounds like a great solution!
Discussed this with @rbuckton and seems that adding a synthesized import is a big change. the other alternative is to make this an error under --isolatedModules
where:
--isolatedModules
is set--module
>= ES2015
Btw, Angular 2 uses this pattern quite a lot, e.g. to detect constructor parameter types. For interface types, it expects the developer to add an extra @Inject
decorator to indicate the required concrete class or injection token, but for class types, there's enough information in the constructor parameter metadata to configure the dependency injector.
I've just tripped across this same issue when trying to use the transpileOnly: true
option in the ts-loader
webpack loader. The warnings about the bogus interface imports are problematic, but the generated runtime test code doesn't seem to be equivalent for non-interface types.
@RoystonS that is my use case as well
@RoystonS Angular's ahead of time compilation (AOT), which seems to be where that Community is going, doesn't even use TypeScript's standards based decorator implementation. They compile away the decorators in a process called lowering which brakes with what was the proposed standard which formed the basis for the TS implementation, and also the current proposed standard, in an incompatible manner.
@aluanhaddad Guesses about directions (with which I partly disagree btw) for Angular doesn’t affect that this is an issue with Typescript today, not ngc, not in the future.
I agree with @staeke - it does look like there's a problem today which it would be good to remedy. Let tomorrow's worries look after themselves :wink:
Is there anything new on this issue? I experienced this now myself and had to search for a while until I found this issue here and was able to fix the issue in my code. For now I used a redefinition of the interface to prevent the incorrect emit:
interface Index_ extends Index { }
// ...
@autobind
private getRowHeight(index: Index_) {
return ...;
}
I think this issue might be set to resolved with the upcoming release of Typescript 3.8 type imports. Using those should always generate the correct code if I'm not wrong, am I?
Just encountered this issue in Nuxt.js when using Inversify decorators. At least it seems like it's the same issue.
In this case @nuxt/types
is a package that does not have any actual modules in it (.js files), only typings (.d.ts) files and I'm importing the Context
interface from it.
Without the injectable()
decorator the @nuxt/types
import and types are stripped from the resulting build, but once I add it webpack throws the following error saying that it can't resolve @nuxt/types
(because as I mentioned above - it doesn't export any modules, only typings) as it tries to actually import it like a real JS module:
And this happens because in the resulting build the @nuxt/types
import remains and webpack tries to resolve it as if it were a real JS import. I added index.js
manually to node_modules/@nuxt/types
to get it to work and it built successfully with the following output:
Thankfully I found this GitHub issue, because it's the only web resource I could find that even remotely explains why this occurs. Maybe this should be documented somewhere more obvious? I guess for now I'll just re-export the interface from somewhere else, but it would be nice to have a real fix where I wouldn't have to do this. Maybe that's not possible, however, so I've also reported this to the package maintainers.
This is on TypeScript 4.2.3 and Nuxt.js 2.15.3
TypeScript Version: 2.4.2
Code (using Angular 4)
tsconfig.json
Expected behavior: Working, no errors. I'd expect no type information emitted for interfaces, as they're only a Typescript compile construct.
Actual behavior: Although this isn't a bug per se, TypeScript generates a decorator for type information, referencing MyInterface from
otherFile
. Only...this interface doesn't exist runtime. So bundlers (webpack in my case) produce a warning for this (something likeotherFile doesn't export MyInterface
). You can workaround this by creating a local type inmy-class.ts
. Or just accept warnings.