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

Provide a way to augment global interfaces from external modules #4166

Closed
RyanCavanaugh opened this issue Aug 5, 2015 · 14 comments
Closed
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@RyanCavanaugh
Copy link
Member

We get "bug reports" of this nature very often:

I wrote this code

interface Window {
  myPlugin: any;
}
let x = window.myPlugin;
class Foo { }

then I added 'export' and I get compile errors, wat?

interface Window {
  myPlugin: any;
}
let x = window.myPlugin;
export class Foo { }

Our current answer today is "Move your stuff to another .d.ts file". This is an OK workaround, but not great. The real sticker is when there's a type defined inside an external module -- it's impossible to use those types to augment a global interface, even though this is a thing that actually happens. It gets even trickier because this encourages people to move types into the global namespace when they didn't want to in the first place, exacerbating the problem.

We need some proposal for how to make this work.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Aug 5, 2015
@kitsonk
Copy link
Contributor

kitsonk commented Aug 6, 2015

Am I being too ignorant, but why wouldn't the following be acceptable?

interface global.Window {
  myPlugin: any;
}
let x = window.myPlugin;
export class Foo { }

My particular use case was extending other global interfaces within a module that was going to try to offload to native if present:

interface ObjectConstructor {
    assign(target: any, ...sources: any[]): any;
}

const assign = 'assign' in Object ? Object.assign : function assign(target: any, ...sources: any[]): any {
    // shim...
}

export default assign;

@zpdDG4gta8XKpMCd
Copy link

Too bad there is no [discussion] tag, gonna add my few cents anyway. It's shocking how a big new feature is coming out of a tiny problem: to get something out of a standard object that is not supposed to be there. If so, what could be easier than this:

export function toSomethingCrazy(window: Window) : SomethingCrazy {
    return (<any> window).stupidPlugin;
}

That's it, the problem has gone. No need for a new expensive feature. Thank to one single guarded place in code.

@mhegazy
Copy link
Contributor

mhegazy commented Aug 7, 2015

@Aleksey-Bykov that does not allow for modeling global side-effects. a library author would want to declare that their library is adding certain definitions to the global scope, so that their users get to use them.

for context, we have rejected this in the past on the basis of discouraging bad practices. however, like them or not, there are existing JS libraries that relay on global extensions to work. the only way to do this today in TS, is to define your global polluters in a .d.ts file, then add a /// reference to it from your module.

@zpdDG4gta8XKpMCd
Copy link

well, can't say you have any other choice other than to add it
👍 for global polluters, I like the idea

@kitsonk
Copy link
Contributor

kitsonk commented Aug 7, 2015

It isn't just global polluters... The other use case is trying to extend the globals without having to load the full lib.es6.d.ts from within a module that is related to that bit of functionality.

@mhegazy
Copy link
Contributor

mhegazy commented Aug 7, 2015

@kitsonk can you elaborate..

@zpdDG4gta8XKpMCd
Copy link

@kitsonk, you sound like such pollutions would be localized in a module where they are introduced (rather than leaking to the global namespace) which sounds opposite to what @RyanCavanaugh originally said

@kitsonk
Copy link
Contributor

kitsonk commented Aug 7, 2015

All that is being suggested is that there is a language mechanism to access global interfaces from within a module. The reasons for it were originally thought to be useless which is why #983 got closed, then re-opened, and now closed again in lieu of this one. There can be many use cases, like having to do with other libraries that pollute the global namespace, which is bad, or my particular use case which is that I wanted to "shim" the global interfaces so I could better handle when the native ES6 functionality wasn't there. So one is the living in the sad reality of the wild west of the web and one might be actually a good thing, though it was better in the end for my use case to extend the global interface (#3889).

@IanYates
Copy link

IanYates commented Sep 1, 2015

Will this help fix code like

//file: frobulatorComponent.ts
namespace Components {
  export namespace Frobulator {
     export class ViewModel {
       public x = ()=> {
        var frobModel: Frobulator.Frob;   //.Frob isn't in intellisense (and this doesn't compile) 
             //since the global::Frobulator namespace is inaccessible   
             //("global::" chosen since it's used in other issues discussing this and nicely conveys what I mean)
        }
     }
  }
}

//file: frobulatorData.ts
namespace Frobulator {
  export interface Frob {

  }
}

I know there's a workaround where I could have

type Frobulator_Frob = Frobulator.Frob;

as the first line in frobulatorData.ts, but then I've got Frobulator.Frob and Frobulator_Frob in my global scope which is a bit untidy. If I have many interfaces in my global Frobulator namespace then I've got a lot of duplication to do.

I can fix this by changing some namespaces of course but global:: would be very handy. As I said in another discussion on this topic, the generated JavaScript is easy in this case - there is none since I'm just worrying about interfaces. I appreciate this would be more difficult with classes but not impossible (some auto-generated type aliases, with some sort of consistent name mangling, at the start of each ts file's emit would do the trick)

Note: I'm just letting VS 2015 build separate *.js files and using ASP.Net bundling to get them on to my page - no ES6 modules, gulp tasks, requireJS or anything like that.

@bgrieder
Copy link

bgrieder commented Dec 5, 2015

I wish we had a "pimping" mechanism à la Scala (http://www.artima.com/weblogs/viewpost.jsp?thread=179766).

An oversimplified view is that when an implicit converter exists between type A and type B, methods of type B can be called on type A (implicit conversion happens in the background). What is great about this mechanism is that it avoids monkey patching and that the conversion only happens when a converter is "in scope/visible" (so conversion does not have to be global). It is also super generic and can be applied to any type.

Implicits are a whole subject in itself, but at least with Typescript if we could have a way of "registering" a converter on a type, that would be great.

@joewood
Copy link

joewood commented Jan 22, 2016

Were the referenced design notes #5292 fleshed out as a proposal?

@vladima
Copy link
Contributor

vladima commented Jan 22, 2016

PR #6213, should already be in master and available in our nightly builds (typescript@next on npm)

@mhegazy mhegazy closed this as completed Jan 22, 2016
@mhegazy mhegazy added Fixed A PR has been merged for this issue and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Jan 22, 2016
@mhegazy mhegazy added this to the TypeScript 1.8 milestone Jan 22, 2016
@trusktr
Copy link
Contributor

trusktr commented Apr 12, 2017

I think I have this problem. I have a d.ts file that I want to extend global with. This works fine:

interface Window {
    foo:number
}

declare module 'bar' {
    export const baz:string;
}

But I'd like to import a type for the Window augmentation:

import SomeClass from './somewhere'

interface Window {
    foo:SomeClass
}

declare module 'bar' {
    export const baz:string;
}

But then I get something like

49 declare module 'bar' {
                  ~~~~~~~~

src/externals.d.ts(49,16): error TS2665: Invalid module name in augmentation. Module 'bar' resolves to an untyped module at '/path/to/bar.js', which cannot be augmented.

with no clear indication what the problem was, until I found the link in @mhegazy's comment #9748 (comment).

It seems not possible to import something into a non-module declaration file that has ambient modules in order to make a type definition? What's the solution? Do I need to make a separate module declaration file and keep ambient stuff in a second non-module declaration file?

@trusktr
Copy link
Contributor

trusktr commented Apr 12, 2017

Ah, yep, that worked. I moved the Window type augmentation into an external module and made it the first import in my entry point, and the other ambient stuff is still in the non-module .d.ts file.

@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
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

9 participants