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

Type-directed emit breaks language design goals. Is it worth it? #6007

Closed
yortus opened this issue Dec 9, 2015 · 7 comments
Closed

Type-directed emit breaks language design goals. Is it worth it? #6007

yortus opened this issue Dec 9, 2015 · 7 comments
Labels
Discussion Issues which may not have code impact

Comments

@yortus
Copy link
Contributor

yortus commented Dec 9, 2015

Project A:

// file: foo-a.ts
import BluebirdPromise = require('bluebird');
export async function foo(): BluebirdPromise { }  //  <== has type annotation

// file: foo-a.js
function foo() {
    return __awaiter(this, void 0, BluebirdPromise, function* () { });
}
exports.foo = foo;

Project B:

// file: foo-b.ts
import BluebirdPromise = require('bluebird');
export async function foo() { }                   //  <== no type annotation

// file: foo-b.js
function foo() {
    return __awaiter(this, void 0, Promise, function* () { });
}
exports.foo = foo;

These two projects differ only by a single type annotation, but they produce different JavaScript code with different runtime behaviour.

(Compiled with v1.7.3 with target=ES6, module=commonjs, noEmitHelpers=true).

(Note elison bug #6003 is also visible here but not the subject of this issue.)

Bug or by design?

I understand this is by design (sorry I can't find a better ref).

However I strongly feel this should be considered a bug, because having a "fully erasable, structural type system" is a core design principle of TypeScript (quote is from the design goals page).

If this is tagged as 'by design', could we please get some sort of statement from the team to clarify where TypeScript now stands on the "fully erasable type system" goal and the "[don't] emit different code based on the results of the type system" non-goal?

@DanielRosenwasser
Copy link
Member

I agree that this goes against the idea of type-directed emit, and I think that's been a great guiding principle, but I'm not entirely sure what a better approach would be. Do you have any suggestions for how to continue with the current approach? @mhegazy and @rbuckton, we were discussing this the other day.

@yortus
Copy link
Contributor Author

yortus commented Dec 9, 2015

For what it's worth this is how I would propose to handle async functions (the feature that has introduced type-directed emit to TypeScript)..... EDIT: Removed lengthy proposal because the proposed polyfilling mechanics seem not in line with the ES7 async function proposal - e.g. see #6068 (comment)

Gist of original comment is here.

@yortus
Copy link
Contributor Author

yortus commented Dec 10, 2015

If we must accept that type-directed emit is here to stay, could we at least fix the inconsistency described in #6027? If that was done, then the type-directed emit of async functions would at least be in harmony with the expected behaviour of type annotations, rather than in conflict with it.

@yortus
Copy link
Contributor Author

yortus commented Dec 24, 2015

I agree that this goes against the idea of type-directed emit, and I think that's been a great guiding principle, but I'm not entirely sure what a better approach would be. Do you have any suggestions for how to continue with the current approach?

Why does TypeScript need to do this at at? ES7 async functions do not support returning custom Promise types - they always return ES6 Promise instances, even if global.Promise has been overwritten (see #6068). I may have missed something, but I haven't yet seen the compelling reason to lose the quality of type erasability in order to implement a feature that is not in line with ES semantics.

For ES3/ES5 there is no ES6 Promise available so there must be a way to polyfill Promise. But why on a function-by-function basis? Wouldn't it be more sensible to polyfill once per project?

@yortus
Copy link
Contributor Author

yortus commented Jan 29, 2016

Thanks team and especially @rbuckton for addressing this for ES6+. With #6631 merged, async functions are more ES7-compliant and return type annotations no longer affect the emitted JavaScript with target >= ES6.

I'd like this issue to remain open however to discuss async function support for ES3/ES5 (via downlevelling), which is currently slated for v2.0. It seems it may still use return type annotations to emit different code (eg see comments here and here). To me this seems unnecessary.

As a web developer targeting older browsers, It would be great to be able to write with async function syntax and have them downlevelled to equivalent ES5. The way I'd like to do that would something like be:

  1. bootstrap code in browser feature-detects Promise support. If absent, assign a Promises/A+-compliant polyfill to window.Promise. Now we can reuse the same Promise-based code regardless of the host's ES-level.
  2. Use an appropriate typings file that ambiently declares the global Promise type.
  3. Write a bunch of async functions same as I would in ES6+ and expect them to pick up the Promise from window, regardless whether it's native or polyfilled.

In particular I don't wan't to have to add a type annotation to every async function telling it what kind of promise to return. Standard practice is to do polyfilling once at startup, not at every function site.

@yortus
Copy link
Contributor Author

yortus commented Nov 10, 2016

Bumping this issue again, since 2.1RC just came out and is the first release with downlevel async/await (congrats on another great batch of features!).

Better to annotate each async function, or just polyfill Promise?

A few weeks ago I brought up the quirks of annotating async functions (here and here). @RyanCavanaugh responded:

async / await is indeed the only place where type-directed emit occurs. This is only considered OK because [...] this is a huge value add, perhaps the highest emit value prop in the language....

However I noticed the 2.1RC blog post makes no mention of this 'huge' feature when demonstrating how to use async/await is ES3/5 environments. Rather, the blog post describes using a runtime Promise polyfill (if necessary) and suitable Promise typings. This is the same approach described in the comment before this one.

So to renew the discussion, may I ask again are non-erasable, non-structural return type annotations really a worthwhile exception in the type system? They don't seem to have been promoted anywhere. As the blog post shows, they aren't even necessary for using async/await in ES3/5. And they have several downsides.

Downsides to type-directed emit of async function annotations

Importantly, there are downsides to using return type annotations for type-directed emit:

  • it goes against a number of TS design goals, most obviously "9. Use a consistent, fully erasable, structural type system."
  • it is a divergence from ES2017 async functions, and is not allowed in ES6+ targets anyway
  • it invites users to write ES3/5 code that cannot be simply updated to an ES6+ target.
  • it is repetitive/non-DRY, requiring manual annotations on individual async functions instead of a single polyfill.
  • it reduces type-system expressivity (an example is here)
  • it is inconsistent with all other type annotations in TS (hence surprising/confusing)
  • the annotations are non-erasable in ES3/5, since adding/removing them can change runtime behaviour
  • the annotations are non-structural in ES6+ as described here

Future TS Versions?

So is there any chance of a future version having async function return type annotations that are consistent with the rest of the language? The breaking change is straightforward for users to fix, and I suspect most users are not even aware of the current behaviour.

The scenario that type-directed emit specifically caters for is when a project has async functions that must produce several different implementations of Promise. How common is that? And it could be achieved through simple wrappers anyway.

@yortus yortus changed the title Changing/removing a type annotation emits different code Type-directed emit breaks language design goals. Is it worth it? Nov 11, 2016
@RyanCavanaugh
Copy link
Member

Polyfilled Promises are rapidly vanishing from the earth; I think we're satisfied with the design as-is here unless there are more concrete manifestations down the line (please raise those as new issues if so)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Discussion Issues which may not have code impact
Projects
None yet
Development

No branches or pull requests

3 participants