-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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 generation for "modularly designed" libraries is hard #5269
Comments
We'd love to help solve your problem. First off, let me ask what you would do differently in just plain ES6? In curious if there's something the type system is preventing you from doing, or whether the friction stems from the way ES6 modules work? For example, some would argue that it is an anti-pattern for module imports to have side effects on objects defined in other modules, such as the Observable prototype, but perhaps that's the only meaningful way to implement the kind of modularity you need. TypeScript currently supports type merging across namespace (formerly "internal module") boundaries, but we haven't gone there with ES6 modules because of the side effect smell. Is it something we should consider? |
BTW, I should add that the pattern of having a library add members to other types (or have the illusion of doing so) is precisely why we introduced extension methods in C#. But in C# it is purely syntactic sugar over rewriting instance method calls into static methods calls, passing the receiver object as the first argument (i.e. |
Honestly, not much. There would be less "adding method stubs" (per the issue I linked), and less "casting", I suppose. It might just be easier to maintain that code and a manually written .d.ts file. I was initially worried about keeping those two things in sync, however, outputting types our users want from our build has proved challenging.
There's two things I'm running into. I can't output type information for methods that could optionally exist on the class. ... however, I can add property "stubs" for those methods. ... BUT... If I do that, then any subclass of that class has to implement those as class Foo {
someMethod: () => void
}
// this doesn't work
class Sad extends Foo {
someMethod(): void {
}
}
// this works, but sucks, because I'm making a new function copy per instance
class Better extends Foo {
someMethod = function() {
}
}
// this works in a more desirable way, but sucks because it's ugly
class Betterer extends Foo {
}
Betterer.prototype.someMethod = function() {
} Ideally, the first subclass
The closest thing I can think of is ES7 function bind. But you'd have to add some additional syntax in TypeScript to let the compiler "know" what a standalone function could "bind" too. Like:
So if I have a string I'm spitballing. Designing languages is hard, I wouldn't pretend to know the edge cases around something like that. It's probably a lot to commit to for a Stage 0 proposal feature... but if ES7 function bind doesn't make it into JavaScript I'll eat my hat. |
Thanks again to you and @RyanCavanaugh for your time. I really appreciate it. |
Does this do what you'd like? interface MaybeMethods {
someMethod?(): void;
}
class Foo implements MaybeMethods {
// some core logic, perhaps
}
// All is good
class Sad extends Foo {
someMethod(): void {
}
} |
@RyanCavanaugh I don't think the class Foo implements MaybeMethods {
someMethod() { throw new Error('not implemented'); }
} It's just a lot of extra stuff for what we need to do. Which is why I'm leaning toward manually creating .d.ts files and going to plain ES6. That comes with it's own drawbacks though. :\ It's a bit of a pickle. |
In TypeScript 1.6 we implemented the ability to merge class and interface declarations (see #3332) so extension libraries can add methods to base libraries. We restricted this capability to ambient classes only, but perhaps we should relax that restriction (there wasn't a strong reason for the restriction in the first place). This would allow you to do the following: // Optional methods
interface Foo {
someMethod(): void;
anotherMethod(): void;
yetAnotherMethod(): void;
}
// Actual implementation
class Foo {
regularMethod(): void {
}
}
class Better extends Foo {
someMethod(): void {
}
} It actually works right now, except you get errors on the two declarations of |
That sounds like a decent solution, actually. |
@mhegazy Opinions? |
This change should be in |
👍 |
Longer term we'd also like to fix the deeper problem of not being able to extend types in other modules. There's some information on our thinking in the design notes, but basically we'd like to get to something like this: // module myoperator.ts
import { Observable } from "../observable";
declare module "../observable" {
// Extend the Observable<T> interface
interface Observable<T> {
myOperator(...): Observable<T>;
}
}
function myOperator(...) {
// Implementation of operator
}
Observable.prototype.myOperator = myOperator; So, when the "myoperator" module is imported it automatically extends the Hope this makes sense. |
Should be |
@DanielRosenwasser ... What is in that tagged version? |
just to clarify, we have forked the release 1.7 into its own branch, and now the version of master, and thus of the nightly builds moved to 1.8. the version of the first nightly ( |
As mentioned in #5292, modules should be able to add declarations in other modules as well as the global scope. We will need a different issue to track the design and implementation of that, but keeping this open for now. |
I'm reaching out to you because over at ReactiveX/RxJS we've been using TypeScript, primarily as a means of generating "correct"
.d.ts
output for our consumers. However, because of the design of our library, that dream isn't really being realized, because of issues outlined here.The specific problem is that we have a need to export a class with type information about what methods it could have. We end up jumping through some strange hoops to make this happen. The reason the architecture of RxJS 5 works this way is simple: not every Rx project needs every Rx operator, and most project build systems don't have granular tree-shaking to remove all of the RxJS operators that a project isn't using. The "low tech" solution is to just provide a plain Observable object to people and let them import operators and decorate the prototype on the one they use.
I think TypeScript has had some real benefits for us, so I don't want to walk away from it if it still makes sense. But if the primary reason we were using it was to generate
.d.ts
files, and we end up jumping through hoops to generate those.d.ts
files, which cause us to have to workaround TSC complaints, we (at RxJS) should at least re-examine our approach.I'd really appreciate experts from your community reviewing the issues we've had and offering up suggestions. Ideally, the RxJS project's woes can offer up to you an opportunity to make the TypeScript experience more intuitive in situations like this.
Thanks for your time.
The text was updated successfully, but these errors were encountered: