microsoft / TypeScript

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

Partial classes #563

Closed disshishkov closed 7 years ago

disshishkov commented 10 years ago

Add support of partial classes. Mixins is not the same, because it's run-time realization. Need compile realization, where partial classes will be combine into one before converting typescript to javascript.

//FileA.ts
partial class ClassA
{      
    constructor(public name: string) {}
    public sayHello(): string { return "hi!"; }
}

//FileB.ts
partial class ClassA
{
   public sayBye(): string { return "by!"; }
}

will be:

partial class ClassA
{      
    constructor(public name: string) {}
    public sayHello(): string { return "hi!"; }
    public sayBye(): string { return "by!"; }
}
falsandtru commented 8 years ago

Do you have other solutions?

FreeFrags commented 8 years ago

I would love to see this added to TS too

hscheibner commented 8 years ago

+1 for code generation purposes (DTOs from a WCF service reference with additional partial class definitions)

nippur72 commented 8 years ago

+1 for code generation purposes. My use case is: I have React components whose render() functions are generated externally via react-templates.

With partial classes I could type-check such functions versus the main class (the Component). Without partial classes, the only thing I can do is binding the function to the main class and hope for the best.

nippur72 commented 8 years ago

With next TS 2.0, I was thinking that some code-generation cases might be covered with the this type feature.

For example in the case I described previously, instead of

partial class MyComponent extends React.Component<any,any> {
    render() {
        ...
    }
}

I can write

function render<this extends MyComponent>()
        ...
}
MyComponent.prototype.render = render;
wendellmva commented 8 years ago

+1 As a developer I want partial classes so that multiple templates can generate/modify the same class spread out over multiple files.

aluanhaddad commented 8 years ago

My comments were specifically in regard to partial classes, not to mixins. The two are fundamentally different. Partial classes are about breaking up physical code, mixins are about breaking up logical behavior into reusable traits enriching other objects at both the type and the value level. I do not think think partial classes are a good idea for TypeScript. Mixins, on the other hand are a good idea, providing increased expressiveness and meshing extremely well with both TypeScript's type system and JavaScript idioms as pointed out by @aleksey-bykov

aluanhaddad commented 8 years ago

@wendellm can you think of a clean way to do this given that ES Modules are physical. Partial classes work well and make a lot of sense in a language like C# where modules are logical not physical. From the point of view of the CLR, namespaces don't exist, it's just that class names can have dots in them (source an interview with @ahejlsberg)

ovrmrw commented 8 years ago

+1 I need partial class!

frogcjn commented 8 years ago

can we extension interface? interface can also have some implemented functions, just like Swift:

interface Rect {
    x: number
    y: number
}

extension Rect {
    area() => this.x * this.y
}

Swift version:


protocol Rect {
    var x: Float {get}
    var y: Float {get}
}
extension Rect {
    func area() -> Float {
        return self.x * self.y
    }
}
hsk81 commented 8 years ago

+1

tsvetomir commented 8 years ago

Code generation and partial classes go hand in hand.

Inheritance is a poor solution to what is essentially a glorified #include directive.

Take Angular 2 for example. It will not read metadata from the parent class, making extension-by-inheritance impractical.

aluanhaddad commented 8 years ago

@tsvetomir that's an Angular issue not a TypeScript issue.

Inheritance is a poor solution to what is essentially a glorified #include directive

Yes inheritance is a poor solution, but the problem is using classes in the first place. They have their place but it is very limited. JavaScript classes are pretty weak and inexpressive compared to classes in other languages.

Mike-Loffland commented 8 years ago

Typescript is not an"Angular" language... Never was

On Jul 23, 2016, at 9:46 PM, Aluan Haddad notifications@github.com<mailto:notifications@github.com> wrote:

@tsvetomirhttps://github.com/tsvetomir that's an Angular issue not a TypeScript issue.

Inheritance is a poor solution to what is essentially a glorified #include directive

Yes inheritance is a poor solution, but the problem is using classes in the first place. They have their place but it is very limited. JavaScript classes are pretty weak and inexpressive compared to classes in other languages.

You are receiving this because you commented. Reply to this email directly, view it on GitHubhttps://github.com/Microsoft/TypeScript/issues/563#issuecomment-234753589, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AJPCIh7n2_0dt00kw-XJv7tc9LB0tPsIks5qYtH4gaJpZM4CcixK.

tsvetomir commented 8 years ago

I don't see the issue as Angular specific in any way. It is a particular case in which partial classes may have helped.

The request originates from past experience with C# and the lack of familiar features. The comparison is inevitable as TypeScript makes no effort to distance itself from .NET. Some form of feature parity is expected by its users.

aluanhaddad commented 8 years ago

The request originates from past experience with C# and the lack of familiar features. The comparison is inevitable as TypeScript makes no effort to distance itself from .NET. Some form of feature parity is expected by its users.

@tsvetomir as a long time .NET developer, and as a passionate C# programmer, I fundamentally disagree.

The article you cited is not authoritative, and does not reflect any official material from the TypeScript team.

Furthermore, it directly contradicts the TypeScript design philosophy as stated by @ahejlsberg in numerous talks, interviews, and posts.

Additionally, the cited article's viewpoint reflects a fundamental misunderstanding as to the nature of TypeScript. Ironically, the commonalities that C# does have with TypeScript are actually the same ones it has with JavaScript. These commonalities are not class-based programming, but rather constructs such as closures and higher order functions.

yahiko00 commented 8 years ago

C# or not, we need partial classes which will help TypeScript getting closer to Javascript's prototype features.

aluanhaddad commented 8 years ago

@yahiko00 TypeScript already has all of JavaScript's prototype features. It is a superset.

There are several problems with the notion of partial classes in TypeScript, including

  1. Unlike C#, TypeScript (JavaScript) class definitions are imperative not declarative. They appear declarative, but they are not. This means they depend upon deterministic execution order.
  2. The ECMAScript module system is based on importing physical not logical code units. For example, lets say I have

    app/my-class-part-1.ts

    export partial class MyClass {
       firstName = "John";
       lastName = "Smith";
    }

    and app/my-class-part-2.ts

    export partial class MyClass {
       fullName = this.firstName + ' ' + this.lastName;
    }

    How can I import this class? Since I cannot import from either module, let us imagine a hypothetical abstraction in TypeScript that allows me to write app/main.ts

    import { MyClass } from './my-class';

    What would this mean? Even if the TypeScript compiler could infer that app/my-class-part-1.ts has to be loaded before app/my-class-part-2.ts, it cannot make the importing of the individual parts invalid nor can it ensure they are imported in the correct order by an asynchronous module loader such as RequireJS or what will eventually be implemented in browsers.

    The whole notion of partial classes is fundamentally at odds with the ECMAScript module system.

Update: It would need to perform arbitrarily complex dependency inference and emit some very odd JavaScript.

yahiko00 commented 8 years ago

TypeScript already has all of JavaScript's prototype features. It is a superset.

I should have expressed myself better. Of course we all know TypeScript is a superset of JavaScript. But I wanted to say that partial classes would help TypeScript classes to get closer to JS prototypes. For the moment, we can just have "static" and non extendable classes, whereas, with a prototype approach, we can extend class-like functions.

Importing partial classes would lead to extends the current ES6 syntax I agree. We could imagine something like this:

import { MyClass } from ['app/my-class-part-1', 'app/my-class-part-2'];
aluanhaddad commented 8 years ago

TypeScript classes are based on ECMAScript classes and thusly suffer from the latter's lack of expressiveness. Consider using functions, modules, "namespaces", or plain objects to accomplish what you need.

yahiko00 commented 8 years ago

Functions is what we want to avoid with the notion of class. Modules or namespaces bring simple extensibility and have not the same expresiveness as TypeScript's classes. Also, a namespace or a module cannot be instanciated. They do not serve the same purpose as classes.

tsvetomir commented 8 years ago

@aluanhaddad I see what you mean regarding modules. A possible solution would be to allow only one of the files to export partial class while the rest contain only partial class definitions. You import it from the place its exported. From your example:

app/my-class.ts

export partial class MyClass {
    firstName = "John";
    lastName = "Smith";
}

app/my-class.part.ts

partial class MyClass {
    get fullName(): string {
        return this.firstName + ' ' + this.lastName;
    }
}

app/main.ts

import { MyClass } from './my-class';

What's missing is a syntax to let the compiler know where to locate the parts. This is not an issue with C# where you process all files at once. This might be a special syntax, as the one used for declaration merging.

It's not required for partial classes to allow arbitrary definitions. They can be limited only to methods, properties and constructors, for example. This makes execution order insignificant and allows the compiler to throw an error if a member is duplicated.

david-driscoll commented 8 years ago

I fail to see why Module Augmentation is not valid for these scenarios. It's worked very well for us with Rxjs5.

You simply create your base class, and then with your code generation, generate all the augmented methods you need on top of the base class. It doesn't matter if the generated code is pretty, no humans are supposed to write it.

Is it a perfect replacement for partial classes? I suppose not, but I don't see partial classes happening unless ECMAScript decides JavaScript needs them. The semantics for how partial classes are linked together between external modules alone hurts my head.

nippur72 commented 8 years ago

@david-driscoll to make it even better, added methods can have a this: argument declared, for a stronger type-checking.

In the cited example of Module Augmentation:

// observable.ts stays the same
// map.ts
import { Observable } from "./observable";
declare module "./observable" {
    interface Observable<T> {
        map<U>(f: (x: T) => U): Observable<U>;
    }
}
Observable.prototype.map = function (this: Observable, f) {
    // here "this" has the shape of the "Observable" class
}
david-driscoll commented 8 years ago

Yeah. Rxjs doesn't quiet use it yet, because it's a 2.0 feature, and 2.0 hasn't been released yet. It's on my hit list later.

aluanhaddad commented 8 years ago

I see what you mean regarding modules. A possible solution would be to allow only one of the files to export partial class while the rest contain only partial class definitions. You import it from the place its exported. From your example:

app/my-class.ts

export partial class MyClass {
    firstName = "John";
    lastName = "Smith";
}

app/my-class.part.ts

partial class MyClass {
    get fullName(): string {
        return this.firstName + ' ' + this.lastName;
    }
}

I like the syntactic concept but the problem is that you have just introduced a new kind of source file that is consumed as a sort of implicit module, looks like a script, and has to be loaded in a specific manner.

What's missing is a syntax to let the compiler know where to locate the parts. This is not an issue with C# where you process all files at once. This might be a special syntax, as the one used for declaration merging.

Declaration merging works for declarations, not source files.

It's not required for partial classes to allow arbitrary definitions. They can be limited only to methods, properties and constructors, for example. This makes execution order insignificant and allows the compiler to throw an error if a member is duplicated.

All of these are commonly order dependent.

Edit: That is to say that defining them modifies the class.

pankleks commented 8 years ago

Probably someone mentioned this already, but another useful case is when you generate part of the class from some code generator, then you wish to manually add some more code directly in your implementation. You don't really wish to do mixing or derived class. It's still one class - just part is generated automatically other part manually.

BTW: if someone needs to generate TS types from C# classes you can check TypeScriptBuilder

Thanks for considering this!

yahiko00 commented 8 years ago

I'm going to try out your TypeScriptBuilder ;)

svallory commented 8 years ago

My suggestion...

What if the compiler didn't try to find all parts? What if I had to import other parts and controlled everything?

/MyClass.partial.ts

export partial class MyClass {
    firstName = "John";
    lastName = "Smith";
}

MyClass.ts

// v- this would NOT throw an error because the definition is marked as partial
import { MyClass } from "./MyClass.partial";

export class MyClass {
    get fullName(): string {
        return this.firstName + ' ' + this.lastName;
    }
}

Then...

  1. If I want to use MyClass I have to import it from MyClass.ts
  2. When compiled, MyClass.partial.ts will generate a javascript which extends MyClass.prototype like showed here before. But it exports a function which receives the prototype to be extended.
  3. MyClass.partial.ts is imported in MyClass after MyClass has been defined and it's extend function is called.

On a side note... there's nothing preventing me from generating the compiled code directly. But I loose all the greatness of Typescript.

aluanhaddad commented 8 years ago

@svallory I think that's definitely the right approach here. Specifically because you say the generated code will export a function which would cause the augmentation, it would work even with asynchronous imports because the functions can be called in the necessary deterministic order.

More generally, and I forget the issue where it was referenced, if assignments to the class prototype affected the shape of the class, there could be many benefits. This would improve mixin libraries, make decorators far more useful, and generally encourage less vertical hierarchies.

It could be tricky for the compiler to track all of this, but it probably could be made to work by being explicit as you suggest.

aluanhaddad commented 8 years ago

It would be an enhancement to the type system, with the partial classes use case falling out of that enhancement.

wongchichong commented 8 years ago

I've did it like the following:

I've did it like the following:
file1.ts
interface ifoo{
   a():void;
}
class foo implements ifoo{
   a() { /*do something*/ }
}
file2.ts
/// <reference path="file1.ts" /> //not necessary
interface ifoo{
   b():void;
}
foo.prototype.b =() => { /*do something*/ }
file3.ts
/// <reference path="file1.ts" /> //not necessary
interface ifoo{
   c():void;
}
(<ifoo>foo.prototype).c =() => { /*do something*/ }
consumer.ts
/// <reference path="file1.ts" />
/// <reference path="file2.ts" />
/// <reference path="file3.ts" />
let f = new foo();
f.a();
f.b();
f.c();
david-driscoll commented 8 years ago

Module Augmentation still solves 90% of the problem here. Given @svallory's examples.

/MyClass.partial.ts

export partial class MyClass {
    firstName = "John";
    lastName = "Smith";
}

MyClass.ts

// v- this would NOT throw an error because the definition is marked as partial
import { MyClass } from "./MyClass.partial";

export class MyClass {
    get fullName(): string {
        return this.firstName + ' ' + this.lastName;
    }
}

The module augmentation would be something like...

MyClass.ts

export class MyClass {
    firstName = 'John';
    lastName = 'Smith';
}

MyClass.generated.ts

import { MyClass } from './test';

Object.defineProperty(MyClass.prototype, "fullName", {
    get(this:MyClass) {
        return this.firstName + ' ' + this.lastName;
    }
});

declare module './test' {
    interface MyClass {
        readonly fullName: string;
    }
}

It's a little more verbose than a partial class, but really the partial class would just be syntactic sugar for this implementation.

The only gotchas today you don't have would be:

EDIT: Keep in mind, this is TS 2.0+ syntax.

aluanhaddad commented 8 years ago

@david-driscoll I agree. I think that is simpler and I like that it doesn't introduce the additional syntax. As you know, I am opposed to partial classes as a specific language feature, but I do think it would be beneficial if TypeScript tracked assignments to prototypes in general and refined the shape of the object accordingly. I think your approach is the right way to split classes across multiple files.

aluanhaddad commented 8 years ago

@wongchichong Your example uses global class names and /// <reference path="..."/>. I don't think that will scale well, although you could use a namespace to improve the situation. Code written as modules (that is external modules) is a very different beast because it involves a loader which needs to be aware of dependencies which are order dependent and implicit. That is why @svallory's suggestion is valuable, it requires a very minimal amount of manual wiring which makes the dependencies explicit.

pankleks commented 8 years ago

What if someone does not use modules?

aluanhaddad commented 8 years ago

@pankleks partial classes are still a bad idea.

aluanhaddad commented 8 years ago

To clarify, I mean they are bad idea in language like JavaScript where class declarations are imperative, not declarative. Even if you're using namespaces, you're going to end up breaking your code into multiple files which means you to have to depend on the script tag order of those files for this to work at all.

In a language like C#, partial classes work well but that's because they're fundamentally different from classes in JavaScript.

It's mportant remember that in JavaScript, class declarations are not even hoisted let alone declarative.

tsvetomir commented 8 years ago

Barring ES6, the "class" concept is defined by TS. JS has no notion of a class at all. So its up to TypeScript to decide what a class can be, no?

saschanaz commented 8 years ago

Barring ES6,

We cannot bar ES6 when discussing TS language features, because one of the TS goals is to follow current/future ES spec.

tsvetomir commented 8 years ago

True that :/

dcworldwide commented 8 years ago

For large systems with code generated classes, i.e. Entities, service layer clients etc, partial classes as per the C# design, provide a very elegant solution when generated code needs to be extended with additional state and behavior. On my present project, where we have ~500 code gen classes, I miss this feature dearly.

yahiko00 commented 8 years ago

I still don't understand why some people are so hostile against a feature that has been proven useful elsewhere, except saying "it is a bad idea".

saschanaz commented 8 years ago

It seems this hasn't been discussed much on esdiscuss, the only thread I found has 9 posts.

aluanhaddad commented 8 years ago

@yahiko00 It is not that I am hostile to the concept. As you say it is very useful in languages such as C#.

As an enthusiastic C# programmer, I find partial classes to be very helpful for certain tasks. However, they do not interfere with the other aspects of the language. They do not break key related abstractions such as namespaces, assemblies, and arbitrarily orderable type declarations.

JavaScript, on the other hand, has a module system that is nascent at best. Classes are a new and, I would argue, as yet not very expressive feature of the language, they need time to grow.

If we take a step back for a moment and consider extension methods, another powerful, and far more useful feature of C#, I would love to see them added to JavaScript but there are fundamental problems with doing so that have yet to be resolved.

When we reach a point where partial classes can be specified without breaking or severely constraining far more fundamental concepts such as modules, I will be all in favor of adding them.

That said, as @SaschaNaz points out, this does need to be addressed in ECMAScript.

saschanaz commented 8 years ago

Maybe anyone interested can open a new thread on http://esdiscuss.org and post the URL here so that we can continue discussion there 😄

PS: Or on more-GitHub-like ES Discourse. PS2: Or on also-GitHub-like-and-more-active WICG.

GrantErickson commented 8 years ago

I too would love to see some construct that allows for code generation for TS. Partial classes work great for this in C#.

Maybe I am missing something, but it seems to me that the core issue is extending a class with access to 'private' variables from somewhere other than in the constructor, preferably from another file.

GrantErickson commented 8 years ago

Based on the discussion it seems like much of the 'safe' TS innovation has been done. Now we are just waiting to see what the ES governance body does because we don't want to break anything going forward. Totally valid, but a little disheartening because I was hoping for a faster moving train with TS.

saschanaz commented 8 years ago

A prerequisite is finding a good desugaring:

Although I previously thought partial classes were pretty silly because you could just add to the class directly, I can see the appeal given that doing so will involve making your methods non-enumerable plus fixing the super binding (the latter of which is currently impossible in ES6).

We first need to figure out a good desguaring for classes in general into compositional, imperative primitives, like the old toMethod. But once we have that, adding some more sugar on top like partial classes might be reasonable, depending on how ergonomic---or not---those primitives end up being.

Not sure what this exactly means but probably like this:

class A {
  foo() { return "foo" }
}
class B extends A {
}

partial(B, class {
  get foo() {
    return super.foo(); // should work
  }
});
saschanaz commented 8 years ago

(Cross posted on esdiscuss)

It seems there is a fairly simple way to implement partial class in pure JS (with ES2017 getOwnPropertyDescriptors). ES people may think a syntax sugar will even not needed only to remove this tiny code.

function partial(base, extension) {
  extension.prototype.__proto__ = base.prototype.__proto__; // to enable 'super' reference
  const descriptors = Object.getOwnPropertyDescriptors(extension.prototype);
  delete descriptors.constructor; // must not override constructor
  Object.defineProperties(base.prototype, descriptors);
  
  return base;
}

Using this on TS requires an interface duplicate to extend existing class type, of course.