microsoft / TypeScript

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

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

Closed RyanCavanaugh closed 8 years ago

RyanCavanaugh commented 9 years ago

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.

kitsonk commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

@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 commented 9 years ago

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

kitsonk commented 9 years ago

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 commented 9 years ago

@kitsonk can you elaborate..

zpdDG4gta8XKpMCd commented 9 years ago

@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 commented 9 years ago

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 commented 9 years ago

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 commented 8 years ago

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 commented 8 years ago

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

vladima commented 8 years ago

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

trusktr commented 7 years ago

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 https://github.com/Microsoft/TypeScript/issues/9748#issuecomment-232822688.

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 commented 7 years ago

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.