microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.14k stars 12.51k forks source link

Why do you support ES6 `for ... of` when the target is ES5 #25027

Closed fis-cz closed 6 years ago

fis-cz commented 6 years ago

Expected behaviour:

Error message that for ... of is not supported when target is ES5

const iterable: string[] = [ "a", "b", "c" ]; 
for (const x of iterable) ...

Current behaviour:

Transpiles to (similary)

var iterable = [ "a", "b", "c" ]; 
for (var x = 0; x < iterable; x++) ...

Description:

As iterators are not supported in ES5 i am curious you support it. When we ask you to support some other feature which requires "advanced transpliation" - basically, replacing the original code with some "polyfill" or "advanced code" you point us out to use babel because you don't support, you don't want to support, that TypeScript is superset or whatever else.

This is confusing guys. You should be consistent in all cases. Because there are limitations when we use iterable pollyfills (in this current case) and its better to get error message than look over the internet and try to solve issues you don't count with. Especially, for dummies this is hell.

the same is valid for promises, async/awaits and more.

It would be definitelly better to:

trusktr commented 6 years ago

If there's a case where TypeScript output can not give spec-compliant functionality in the output, it should by default throw an error, and should be opt-in.

kitsonk commented 6 years ago

TypeScript is a syntactic transpiler. This has been the case since day one with TypeScript. When the target is ES5, it supports all the language syntax of future versions and down emits it to the target specified.

For example async/await are support in ES5, ES6 and ES2016 though they weren't formally introduced until ES2017.

There has been a clear line... It transpiles only syntax, not inject functional polyfills. Admitedlly, there are some blurry lines, like async/await and downlevel emit iterators, where there is a presumption that there is available some functional polyfills to build upon (global Symbol and Promise), at that point they become "opt in" because they have prereqs outside of the language syntax, but when TypeScript can safely down emit using the downlevel language, it does it.

To do what you suggest, not support it unless it is supported in the target, you would hinder TypeScript to disallow supporting const and let when the target is ES5. That would be crazy. How is that different from for...of of arrays?

NN--- commented 6 years ago

It is confusing since for..of with downlevelIterators for arrays works very slow, while ES5 for,,of transpiles to fast for-index.

DanielRosenwasser commented 6 years ago

@kitsonk has covered the main point: we do syntactic compilation and don't provide user-land APIs that would be covered by polyfills.

We had a similar conversation in #1641. At this point, Babel 7 does have a TypeScript plugin. So if you're hankering for auto-polyfills and preset-env, the option is available to you.

Check out more here.

RyanCavanaugh commented 6 years ago

I don't even understand the OP because we do issue an error when a for/of's operand is an Iterable and the target is ES5 (unless --downlevelIteration has been enabled).

fis-cz commented 6 years ago

@kitsonk I get your idea, I partly agree with it but there is JavaScript hell since I remember and this is not helping so much.

@DanielRosenwasser This is not just about babel. Browserify supports TS, gupl supports TS, everybody does. Do we really need tons of tools in our toolchains? Sure we do, but sometimes it is hard to get everything work and these slight inconsistences are doin'g things much worse. It takes time to find out that it is better to transpile to latest ES using TS then use the babel to downlevel and know what output is expected. To be honest, I would like to avoid using babel in my toolchain.

@RyanCavanaugh If --downlevelIteration is not enabled by default you are not correct. I tried locally, I tried here

https://agentcooper.github.io/typescript-play/?target=1#code/MYewdgzgLgBFCm0YF4YG0YCICGmA0WARvlsJjALoDcAUAGYgBOMAFKJLLCHXIlAJQwA3jRhiAJiGABXALbwwUAHQB3RgEsELAAYASIVAC+MADyFmAegB82-rUM0gA

and also on your playground and it works everywhere in the same way. I have no clue if your playground or the other one is using --downlevelIteration, but I know what I am using in my TS config and my command line.

By the way, see your doc (at the bottom of the page): https://www.typescriptlang.org/docs/handbook/iterators-and-generators.html. You allow for array but not for Symbols. Its another exception we have to remeber or we have to look for in the documentation over or the internet. What this helps to in the end?

I updated the initial post to be clear what I mean. I ment mainly arrays, not Symbols. And that the for ... of should not be allowed even for arrays.

RyanCavanaugh commented 6 years ago

for ... of should not be allowed even for arrays.

We can accurately downlevel it. Why disallow it?

kitsonk commented 6 years ago

I get your idea, I partly agree with it but there is JavaScript hell since I remember and this is not helping so much.

So disallow const and let is your suggestion when targeting ES5? As Ryan points out, TypeScript won't let you do things it can't do. It can provide a downlevel emit for ES3 and ES5 for for...of. If it requires other functionality other than the target language syntax, it errors, unless the flag asserting that the functionality is available is enabled.

You allow for array but not for Symbols. Its another exception we have to remeber or we have to look for in the documentation over or the internet. What this helps to in the end?

It isn't an exception, it is entirely consistent design philosophy and one where TypeScript will error and if you try to for..of over a non array or string, you get the error message: Type 'Map<any, any>' is not an array type or a string type. Use compiler option '--downlevelIteration' to allow iterating of iterators. You can then figure out if you need to find out more to understand the error, but it is pretty clear.

fis-cz commented 6 years ago

So disallow const and let is your suggestion when targeting ES5?

You can also say you can completely remove protected and private as it is not supported by ES at all. Thats true, but it is helpful to programers to define their public and private api's without need of _. Private/protected methods are checked in the transpile time to help us prevent generating bugs.

In opposite to let and const which allows the transpile time (but not runtime in case of ES5)

as @kitsonk correctly noted in his bellow post, TS ensures the same in the runtime.

variable scope / readonly checking, for ... of brings nothing to typed language targeting the es5. Just problems and confusion when you start going deeper.

It isn't an exception, it is entirely consistent design philosophy

In this particular case, you are doing the babel's work although you many times told us you don't want to. And this is also confusing. Once you do, other time not.

As I have already moved to TypeScript:ESNext / Babel stack what makes transpilation of bigger projects longer (as any additional external tools in the toolchain usually do) I don't care anymore.

It is just my recommendation.

kitsonk commented 6 years ago

You can also say you can completely remove protected and private as it is not supported by ES at all.

I wasn't suggesting removing anything, I was pointing out the problem with what you were proposing.

In opposite to let and const which allows the transpile time (but not runtime in case of ES5) variable scope / readonly checking

Actually, TypeScript changes the emit to ensure block scoping is preserved. So it ensures the runtime scoping as the code was authored.

let foo = 'bar';

if (true) {
    let foo = 'wookie';
    console.log(foo);
}

console.log(foo);

transpiles to:

var foo = 'bar';
if (true) {
    var foo_1 = 'wookie';
    console.log(foo_1);
}
console.log(foo);

In this particular case, you are doing the babel's work although you many times told us you don't want to.

Babel is a general purpose JavaScript to JavaScript compiler, including automatically providing functional fills. TypeScript is a syntactical transpiler and type system. It isn't doing Babel's job, they are two seperate projects with two seperate goals. If anyone has referred to not doing Babel's job it is in the context that some goals of Babel are not aligned to the goals of TypeScript. And TypeScript had its goals before Babel was a thing.

Lastly, for the record, your usage of you indicates you think I might be part of the TypeScript team. I am not. I am just a long time user of TypeScript.

fis-cz commented 6 years ago

Actually, TypeScript changes the emit to ensure block scoping is preserved.

True, sorry, but you know what I wanted to say. And you are correrct it moreover,in addition to transpile time checkings, ensures the same during the runtime.

TypeScript is a syntactical transpiler and type system. It isn't doing Babel's job... ... goals before Babel was a thing

But it is changing now, correct?

in this case, of for ... of it is doing babel's job. And it does not bring anything to type checking, just transpiles es6 syntax to be valid within the es5. It is not TypesScript syntax. And this should not be transpiled until you use --downlevelIteration. From my perspective, the same should be for Promises, async/awaits, Maps, Sets, whatever is not directly used by the TypeScript for type checking.

... you think I might be part of the TypeScript team

Ye, sorry I assigned you there ;)

... I am just a long time user of TypeScript.

So you know it very well. But for beginners (as I am), especially for those coming from different languages (such as C#) there are many confusing things.

kitsonk commented 6 years ago

There is a clear distinction. Promise, Map, Set, Proxy, Symbol plus object, string and array extras are functional fills of global objects. for...of, const, let, async/await, generators, arrow functions, destructuring, default arguments, classes, method property shorthand are all syntactical language features which can be down emitted. All of these syntactical features don't relate directly to a type system, but TypeScript has supported them. It has been consistent in what it introduces, and has become more rigid on when it is acceptable to introduce them as well.

When they require functional fills to ensure a valid down level emit, they are behind a flag.

There is the third category, which TypeScript has generally avoided for a long time, which are syntactical enhancements which don't directly relate to the type system, like enum and the legacy module syntax (which was the lesson of adopting syntax too early).

fis-cz commented 6 years ago

I partly agree again. Anything you mentioned is supported because it is part of ES specs or it is in later phase of the TC39 review. I have no problem with fills, but only when downlevel flag is specified explicitly OR until it is directly required by type checking system itself.

kitsonk commented 6 years ago

So that means const and let should be under flags, along with arrow functions, destructuring, default arguments, classes, method property shorthand?

fis-cz commented 6 years ago

In the end, If I think about it, yes. All syntax which is out of scope of the transpilation target and is not a TypeScript syntax itself (types, interfaces, enums, whatever) should be transpilable only with --downlevelIteration option, otherwise an error should be thrown. Or, the downlevel could be removed and error thrown in all cases. Or, the downlowel could be removed and everything should be filled/polyfilled by TypeScript itself, while 3rd party tools would not be required at all. It is only the one way how to avoid to have multiple fills from various tools, just because of one tool supports it and another not. Although it should not matter in the end, because it will just work, it can have big impact on code size as mutliple fill libraries from multiple tools can be included.

fis-cz commented 6 years ago

By the way, I don't think this should be labeled as a question, rather bug, improvement or feature requirement.

avonwyss commented 6 years ago

All syntax which is out of scope of the transpilation target and is not a TypeScript syntax itself (types, interfaces, enums, whatever) should be transpilable only with --downlevelIteration option, otherwise an error should be thrown.

In its early days TS was somewhat ahead of the ES standardization process, and several features which were available in TS have been standardized and included in ES6 and newer. So where do you draw the line between "TypeScript syntax" and "ES syntax"? Things may be TS syntax and later become ES syntax, and it would not make sense to retroactively hide these behind compiler flags.

And what would you do with modules and type imports? When targetting ES5, would modules be "TS syntax" or not in your point of view?

That being said, @kitsonk has explained well where the line is: global polyfills are not provided by TS, but it does support every feature it can through transpilation. If it cannot provide support it throws an error. This allows the user of TS to use a consistent codebase which is also pretty future-proof.

fis-cz commented 6 years ago

@avonwyss In its early days the computer was only on the paper and the dream of few math guys. As far as I know, most of the features implemented in TypeScript are and always were implemented in common with TC39 proposals.

It seems you want to go through the all lang specs 1 by 1 and ask me how I would like to have it. It is not just about me.

Modules are transpiled according the target module specification. If we want to have a module support it is necessary to have a standard way to define, resolve and load them. I think the current design is fine and there is no reason to change it. Although it follows es6 proposal for modules (curently ES2019 with dynamic imports), it fully supports ES3+ by implementing CommonJS spec (and others) - basically no ES ways at all. BUT! We can explicitly configure this in tsconfig and we exactly know what we can expect.

I can tell you it is really confusing, especially in the beginning and it takes long time to get a knowledge, what is internally filled, what is not, why 3rd party libs are time to time required or not. And why them does not work as expected when they are used in down level version of ES. As all of us discussing here we are in for a while it can have sense less or more, for beginners this is really hard.

By the way, is there a real reason the TypeScript is not full featured transpiler including everything needed for full further and backward ES compatibility? Do we really need to do parse, lex, transpile and emit 10th times in our build toolchains? Sometimes I feel like I need more power than to compute bitcoins... (this is not case of the TS itself as the whole process is really fast).

RyanCavanaugh commented 6 years ago

Disagreeing with design goals is not a bug / feature request / suggestion. TypeScript has provided downlevel syntactic transforms since its inception and we don't intend to remove them or make them errors; if you want a non-downleveling transformer there are many more configurable options available.

fis-cz commented 6 years ago

@RyanCavanaugh Thats is.