Skip to content

Provide a way to load Bluebird globally in es6 compilation target. #12382

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

Closed
d-ph opened this issue Nov 19, 2016 · 11 comments
Closed

Provide a way to load Bluebird globally in es6 compilation target. #12382

d-ph opened this issue Nov 19, 2016 · 11 comments

Comments

@d-ph
Copy link

d-ph commented Nov 19, 2016

TypeScript Version: 2.1.1

Code

  1. Either git clone -b e5-and-core-js-case https://github.com./d-ph/typescript-bluebird-as-global-promise.git or git clone -b vanilla-es6-case https://github.com./d-ph/typescript-bluebird-as-global-promise.git
  2. cd typescript-bluebird-as-global-promise
  3. npm install
  4. Edit node_modules/@types/bluebird/index.d.ts by adding export as namespace Promise; above the export = Bluebird; line. (please ignore the fact that you're hacking 3rd code for a second)
  5. npm run tsc

Expected behavior:
Compilation succeeds. Promise is detected to be declared by bluebird.d.ts.

Actual behavior:
Tsc says, that lastly, delay and finally don't exist on type Promise.

More info:
Bluebird polyfills the global Promise, but Typescript ignores that (i.e. ignores the export as namespace Promise; line) when Promise is already defined by either the lib.es2015.promise.d.ts es6 declaration file or the core-js declaration file.

I think the problem doesn't lie in Typescript's ignoring the Bluebird's export as namespace Promise;, but in some sort of precedence order, when global symbols are being declared by .d.ts files. What I'd like to be able to do is to somehow tell Typescript, that Bluebird is my effective Promise provider. Maybe this could be achieved by making Typescript respect the order of @types listed in tsconfig.json's compilerOptions.types array? I.e. the last item in that array wins any global namespace conflict?

This problem most likely affects any polyfilling library in the wild, where the polyfill is.. more convenient to use than the standard solution, so devs decide to use it even after starting using newer js standard.

Btw. this ticket is sort of blocked by #10178 at the moment.

Thanks.

@aluanhaddad
Copy link
Contributor

First of all do not add the line

export as namespace Promise;

Add this line instead

export as namespace Bluebird;

Then add a file called globals.d.ts (or whatever you want to name it). And put it at the root of your project. Add this content to the file:

interface Promise<T> extends Bluebird<T> {}

interface PromiseConstructor {
  delay: typeof Bluebird.prototype.delay;
}

Add the following to your tsconfig.json

{
  "compilerOptions": {
    "lib": [
      "es5",
      "dom"
    ]
  }
}

That is all you should need to do.

@d-ph
Copy link
Author

d-ph commented Nov 20, 2016

Hi Aluan,

Thanks for your answer. It led me to the right direction.

Your solution works for es6, however there are some edge cases, when it doesn't. Also, I'd like devs to just npm install ?? to get up to speed with Bluebird, without requiring them to manually patch the PromiseConstructor (which has far more static methods than just delay).

I'm trying to prepare a bluebird-global.d.ts, that would do, what you advised me to do, for all devs. I got it working, but could you please take a look at it and like "sign it off" maybe? You seem to be up to date with current best practices for writing declaration files, so I'd like to hear your opinion on this. Just tell me whether you see anything wrong with it, please.

Steps:

  1. git clone -b fix-attempt-2-import-bluebird-d-ts https://github.com./d-ph/typescript-bluebird-as-global-promise.git
  2. cd typescript-bluebird-as-global-promise
  3. npm install
  4. cp -R dt-fixes/* node_modules/@types/
  5. npm run tsc

Differences and implementation details:

  1. I didn't use export as namespace Bluebird; in bluebird.d.ts. The reason is, that I don't need it and also because it's actually telling a lie (Bluebird symbol is not available in runtime)
  2. Added bluebird-global/index.d.ts it imports bluebird.d.ts and patches the global namespace with Bluebird's functions (there will be more of them). I needed to add the new<T> and var Promise: PromiseConstructor, because otherwise it doesn't compile in vanilla es5 target. Also, the new<T> 's signature is different than the standard's, but is seems to work nevertheless.
  3. I don't limit the "libs" in tsconfig.json. The reason is, that it works regardless and that this would be annoying to list all the libs, but the lib.es2015.promise.d.ts

What do you think about it?

@aluanhaddad
Copy link
Contributor

First of all I apologize for any confusion due to my example which was actually kind of a hack.
I definitely agree that it is much better remove the export as namespace so as to accurately reflect the runtime behavior.

Part of the problem with Bluebird is that it has static members such as all which differ slightly from the the analogous members of PromiseConstructor. So honestly my solution was a bit of a hack to get around that by making Promise<T> extend Bluebird<T>.

This is made harder by the fact that type declarations are not open ended like interface namspace and class declarations. This is further exacerbated by the fact that the Promise definition for core-js is expressed as a decomposition into interfaces representing the static and instance sides of the class, while the other, Bluebird, is expressed as a class and a nampspace.

I'm not sure if this is relevant, but after having some difficulty over the past few months using the declaration files for core-js, I have transitioned to generally just using the lib.es2017.*.d.ts declarations that are bundled with typescript. The main advantage to this is that these typing are generally more accurate, being maintained by the TypeScript team to match shape of APIs from the emerging specifications, and especially that they are modular so that you could for example exclude the Promise portion itself.

I will review what you have in greater detail as soon as I get some time.

@d-ph
Copy link
Author

d-ph commented Nov 21, 2016

Yeah, I noticed, that some of Bluebird's methods are slightly different than the standard's. Will see how it goes.

I haven't thought of using the lib.es201x.*.d.ts declaration files instead of core-js's. That's a really useful piece of advice. Thanks.

And thanks for taking time to look at my solution. Just wanted to mention, that I'd just like you to take a look at the bluebird-global/index.d.ts only. Also, that file is more of "proof of concept" than commit-able solution. E.g. it's missing the DefinitelyTyped's doc block a the top. I'd just like you to take a gander and see whether you can spot anything obviously wrong.

Thanks.

@d-ph
Copy link
Author

d-ph commented Dec 29, 2016

Update: The code is now in DefinitelyTyped repo (link). I'm pretty happy with it, so there's no need to check anything anymore. I'm removing the proof-of-concept git repo now and closing this ticket.

Thanks for all the tips 👍

@d-ph d-ph closed this as completed Dec 29, 2016
@MichaelTontchev
Copy link

@d-ph , should we add bluebird-global to the types only if we have already declared a types config option? Otherwise, doing so kills all our other types.

@d-ph
Copy link
Author

d-ph commented Apr 2, 2017

Hi @MichaelTontchev

Sorry, but I don't understand the problem you're describing. Could you give me a steps-to-reproduce, so I can better understand the problem you're facing, please?

{
  "compilerOptions": {
    "types": [
      "bluebird-global"
    ],
  }
}

I recommend adding bluebird-global to the compilerOptions.types array, so it's "globally imported" in your ts project. In other words, you don't need to import it yourself over and over again in every source file. Also, it makes all Promises, constructed in 3rd party code you use, be typed as Bluebird (e.g. you'll be able to use .finally(fn) on Promises returned from 3rd party libs).

If, however, you'd rather not put bluebird-global in compilerOptions.types, then that's fine. The consequence is, that:

  1. You need to import bluebird-global (or just use the original bluebird) in each of your files, to use Bluebird.
  2. You'll need to "wrap" every Promise returned from 3rd party code with Bluebird's constructor function in order to get Bluebird's functionality on those promises.

If you decide not to put bluebird-global in compilerOptions.types, then I'd like to know the use case under which this causes problems. More specifically, what you mean by doing so it kills all our other types.

Thanks.

@MichaelTontchev
Copy link

Thanks for your long response, @d-ph !

[Read the following, but also read my realization after the "--------" please :) ] According to https://www.typescriptlang.org/docs/handbook/tsconfig-json.html , "If types is specified, only packages listed will be included." I am interpreting this to mean that if our project currently does not have a types config declaration, then if we just add bluebird-global, all other type lookups will fail.

Indeed, when I add

"types": [
            "bluebird-global"
        ]

to my tsconfig, I immediately get compilation errors.


But now I'm realizing that all that meant was that I needed to add jQuery as a type as well, while all of my other type lookups are explicit with import statements.

So is my understanding correct that for types that need to be globally available without an import, I put them in the types option in the tsconfig, but for types that I import, I can just continue using the normal import?

@MichaelTontchev
Copy link

MichaelTontchev commented Apr 2, 2017

Also, another thing: I've followed your instructions, but it doesn't seem like bluebird is getting included in my bundle.

So I've done npm install @types/bluebird-global --save-dev , and I've added bluebird-global to the types option in the tsconfig file, but when I look at the bundle, it hasn't added bluebird. When I run "Promise" in the Chrome dev console, I see that it's still the native promise.

Even if I do import * as Promise from 'bluebird'; and put Promise; in my entry file (to force it to add the package), it does include bluebird in the bundle but it doesn't replace the native Promise with bluebird's promise.

Any ideas? What am I doing wrong?

@MichaelTontchev
Copy link

If I do the following, finally bluebird replaces the native promise:

import * as bluebird from 'bluebird';

(window as any).Promise = bluebird;

But I have a feeling this isn't the right approach...

@d-ph
Copy link
Author

d-ph commented Apr 2, 2017

Re: Why Promise is not polyfilled (replaced) in the runtime despite my including bluebird-global as advised.

Because this is your job to ensure, that what bluebird-global tells TypeScript is true :) I.e. this is your job to actually include bluebird.js in the runtime either by a <script /> tag in the html (which would automatically replace the global Promise symbol in the runtime for you), or by importing it in your js entry files. My webpack entry file:

/*
 * Promise
 */
import * as Promise from 'bluebird';
import 'expose-loader?Promise!bluebird';

Promise.config({
    longStackTraces: true,
    cancellation: true,
    // monitoring: true,
    warnings: true
});

The job of @types/bluebird-global is to tell TypeScript, that the global Promise symbol is not the standard Promise, but the Bluebird Promise. And that's it.

I admit it's not ideal. Few things in webdev are ideal.

The canonical way of using Bluebird in TypeScript, is to use the @types/bluebird and to not touch the global Promise symbol. In my humble opinion, this is absolutely required when writing js libraries, but cumbersome, when you code your own project, that won't be included in any other (i.e. it's not a library). There's nothing stopping you from doing things the right way (i.e. not overriding the global Promise, but instead importing "bluebird" in every source file and wrapping every Promise returned from every 3rd party library you use with the "bluebird" constructor). I might sound defensive here -- I'm not. I just want you to understand what's what. I should probably add a "Use at your own risk" clause in the @types/bluebird-global source file.

Re: types: [] overrides the default behaviour of TypeScript including all node_modules/@types by default.

You're right here. I wasn't aware of this behaviour. Probably because bluebird-global is the only global thing I use ;p That's intriguing though, because this would mean, that bluebird-global should JustWorkTM without the types: [] thing. Which indeed it does JustWorkTM. Please don't use the types: [] thing then. I will remove the recommendation from the @types/bluebird-global. Thanks for sharing this info.

Thanks.

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants