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

TS Proposal : abstract class expression, generic metadata, constructor typing, abstract class decorators, abstract members decorators #20887

Closed
SalathielGenese opened this issue Dec 25, 2017 · 19 comments
Labels
Duplicate An existing issue was already created

Comments

@SalathielGenese
Copy link

SalathielGenese commented Dec 25, 2017

TypeScript Issue-Proposal

TypeScript 2.6.2

What follow are proposals for TypeScript. If follow a well know discussion, rather official #3628. It propose handling [1] abstract class decorators, abstract members decorators, generic metadata, and [2] constructor typing, and abstract class expression

[1]: Abstract class decorators, abstract members decorators, generic metadata

As to me, it is clear that by no mean should Interface exist at runtime as far as ECMA doesn't consider it worth runtime-life. However, need for this feature is more and more demanded, the reason is simple for me: more poeple are moving from JAVA to TypeScript (steping over Javascript) and so will it be with Kotlin, which they bitterly defend already: I wonder if they face the same challenging Interface DI when transpiling to Javascript.

Initial scenario

export interface CrudLike<E extends Entity>
{
    search(id: ObjectID): PromiseLike<E>;
    drop(entity: E): PromiseLike<void>;
    save(entity: E): PromiseLike<E>;
    search(): PromiseLike<E[]>;
}
public class ProductRepository implements RepositoryLike<Product>
{
    @Inject()
    public categoryCrud: CrudLike<Category>;

    @Inject()
    public productCrud: CrudLike<Product>;
}

Writing such code is very difficult, not to say impossible in typescript, at least, using the official transpiler. Keeping in mind that no interface should be decorated at the moment, my suggest should make this possible :

Scenario -001

@CustomCRUD()
export abstract class Crud<E extends Entity> implements CrudLike<E>
{}

In this case, generated metadata following @CustomCrud() should describe methods and attibutes from CrudLike which the abstract class implements.

Scenario -002

@CustomDAO()
export abstract class Dao<E extends Entity>
{
    @SqlRequest('INSERT INTO ...')
    public abstract create(entity: E): PromiseLike<E>;

    @SqlRequest('SELECT E.* FROM ...')
    public abstract search(): PromiseLike<E>;

    @SqlRequest('SELECT E.* FROM ... WHERE id = E.id')
    public abstract search(id: number): PromiseLike<E>;
}

This second senario just improves the first one, enabling us to write things like :

@DiFactory(daoFactory)
@CustomDAO()
export abstract class Dao<E extends Entity>
{
    //...
}

function daoFactory<E extends Entity, G extends {new(...args: any[]): E}>(generic: G): Dao<E>
{
    //...
    // the injector may rely on this factory to forge clean instances
    // tis factory can even rely on metadata to enforce adequate method implementations
    //...
}

// ...and later on
class UsefulClass
{
    @Inject()
    private productDao: Dao<Product>;

    @Inject()
    private categoryDao: Dao<Category>;
}

A word to conclude : the proposal

It come up that this would be possible :

  1. When abstract class will support decorators
  2. When decorator could be applied to abstract members of abstract classes
  3. When generated metadata will describe even generic types (If TS support complex generics, it means they can be modeled and represented, thus supported)

I really wonder why such constraints were first enforced !

[2]: constructor typing, and abstract class expression

TypeScript documentation proudly exhibit this fact :

One of TypeScript’s core principles is that type-checking focuses on the shape that values have. This is sometimes called “duck typing” or “structural subtyping”. In TypeScript, interfaces fill the role of naming these types, and are a powerful way of defining contracts within your code as well as contracts with code outside of your project.

Please, stop when I am wrong. This statement also implies that the following are identical :

interface NameableLike { name: string; }
// and
type NameableLike = {name: string; };

Scenario -001 : Constructor typing

class Type {}
interface TypeConstructor
{
    new(): Type;
}

function fn_001(clazz: TypeConstructor): TypeConstructor
{
    return class extends clazz {};
}

function fn_002(clazz: {new(): Type}): TypeConstructor
{
    return class extends clazz {};
}

/**
 * 
 * Tests serie #001
 * 
 */
fn_001(Type);
fn_002(Type);

/**
 * 
 * Tests serie #002
 * 
 */
class AnotherType {}
interface AnotherTypeConstructor extends TypeConstructor
{
    new(): AnotherType;
}
fn_001(AnotherType);
fn_002(AnotherType);

The decorators fn_001 and fn_002 where type enforced to work with Type and are now traped to accept even AnotherType. If one say this design is backed by no logic, I answer that typing was introduced for best practice and early violation detection. The question that springs up now is [W]hat typescript construct will ensure for sure that our constructor is for Type and Type only.

Scenario -002 : abstract class expression

The following scenario is a simplified image of my actual case, which is much more complicated.

abstract class MuchAbstraction
{
    public abstract get identifier(): string;
}

abstract class KindaLessAbstraction extends MuchAbstraction
{
    // `identifier` cannot be implemented here as it is generated at runtime
}

function fn_001(clazz: {new(): MuchAbstraction})
{
    return class extends clazz
    {
        public constructor()
        {
            super();
            // someLogic(); //...
        }
    }
}

Take note that even fn_001 SHOULD NOT implement identifier accessor as it is not its reponsibility: another decorator ensures it. As some may have seen, abstract keyword isn't allowed in class expression which is a limitation.

The following isn't a solution !

interface MuchAbstractionConstructor
{
    new(): MuchAbstraction;
}

function fn_002(clazz: MuchAbstractionConstructor)
{
    return class extends clazz // same error for abstract classes
    {
        public constructor()
        {
            super();
            // someLogic(); //...
        }
    }
}

What follow works well, BUT a 'one decorator - all purpose' isn't a great design.

function fn_003(clazz: MuchAbstractionConstructor)
{
    return class extends clazz
    {
        public constructor()
        {
            super();
            // someLogic(); //...
        }
        
        public get identifier(): string
        {
            return <any>void 0;
        }
    }
}

However, the following calls are rejected : ask me why ?

// 'typeof MuchAbstraction' is not assignable to parameter of type 'MuchAbstractionConstructor'
fn_003(MuchAbstraction); // error

// 'typeof KindaLessAbstraction' is not assignable to parameter of type 'MuchAbstractionConstructor'
fn_003(KindaLessAbstraction);  // error

In fact, none of the above fn_001 and fn_002 works when called.

function fn_004_from_fn_001(clazz: {new(): MuchAbstraction})
{}
function fn_004_from_fn_002(clazz: MuchAbstractionConstructor)
{}

fn_004_from_fn_001(MuchAbstraction); // error
fn_004_from_fn_002(MuchAbstraction); // error

However, the following works great :

class NoAbstraction extends KindaLessAbstraction
{
    public get identifier(): string
    {
        return <any>void 0;
    }
}

fn_003(NoAbstraction); // function fn_003(clazz: MuchAbstractionConstructor) { /** some logic **/ }

Conclusion: Even constructor construct to target static side doesn't accept abstract classes. Where then is the shape or duck typing proned in TypeScript documentation? Or where it does stop (I have not found in docs).

This somehow take us to the question in scenario -001 : constructor typing.

Nonetheless, I was able to come up with something that works :

function fn_004_from_fn_003(clazz: typeof MuchAbstraction)
{}

fn_004_from_fn_003(MuchAbstraction);

The following implementation leads to unsuspected results : everything works well.

function fn_005(clazz: typeof MuchAbstraction)
{
}

let a = fn_005(MuchAbstraction);
fn_005(KindaLessAbstraction);
fn_005(NoAbstraction);

This encouraged me to go a little further :

function fn_006(clazz: typeof MuchAbstraction): typeof MuchAbstraction
{
    return class extends clazz
    {
        public constructor()
        {
            super();
            // someLogic(); //...
        }
        
        public get identifier(): string
        {
            return <any>void 0;
        }
    }
}

const MyLessToNoAbstraction: typeof MuchAbstraction = fn_005(MuchAbstraction);

If, Ôh if abstract classes were allowed in class expressions, I could have wrote something like... Being confident of its validity at rutime.

function fn_007(clazz: typeof MuchAbstraction): typeof MuchAbstraction
{
    return abstract class extends clazz
    {
        public toString(): string
        {
            return `${ this.valueOf() } identified as #${ this.identifier }`;
        }
    }
}

const MyLessToNoAbstraction: typeof MuchAbstraction = fn_005(MuchAbstraction);

A word to conclude : the proposal

The proposal is as follow :

  1. Support of abstract class in class expression

  2. Better constructor typing:

    • Either by adding a core generic type. Something like what is not valid yet now: type Constructor<T> = typeof T; (because T is used here as a value !?)
    • Or by constraining that constructor interfaces to build the same type, even when extending another constructor type : this means that the extending can just overload constructor by parameters.
    class Type
    {
    }
    
    interface TypeConstructor
    {
        new(): Type;
    }
    
    class AnotherType
    {
    }
    
    interface AnotherTypeConstructor extends TypeConstructor
    {
        new(): AnotherType;  //wrong: expected Type instead of AnotherType
    }
    
    interface ProposedTypeConstrustor extends TypeConstructor
    {
        new(timestamp: number): Type; // ok (at this proposal stage)
    }
  3. By the way, it should be considered adding a name attribute to constructor types (i.e {new()...} and its interface avatar). So as to ease the following.

function fn<T, C extends {new(): T}>(clazz: C)
{
    return {[clazz.name]: class extends clazz
    {
    }}[clazz.name];
}

Summary

The proposals are summed as follows :

  1. Decorator support for abstract class
  2. Decorator support for abstract members (of abstract classes) with the third parameter being something like AbstractedPropertyDescriptor or a fourth parameters as boolean (why not? let's be crazy this far)
  3. Generate metadata that describe up to generic types
  4. Support of abstract class expression
  5. Better constructor typing:
    • Either by adding a core generic type. Something like what is not valid yet now: type Constructor<T> = typeof T; (because T is used here as a value !?)
    • Or by constraining that constructor interfaces build the same type, even when extending another constructor type
  6. Considered adding a name attribute to constructor types (i.e {new()...} and its interface avatar).
Thanks,
Eager to hear from you all,
@SalathielGenese from Squall.IO.
@PFight
Copy link

PFight commented Dec 25, 2017

The first scenario has implemented in typedin with existing TypeScript features.

@SalathielGenese
Copy link
Author

SalathielGenese commented Dec 25, 2017

@PFight , I've a look at typedin and :

  1. typedin uses class not interface when injecting.
  2. The very first scenario inject
    • two (02) interfaces
    • expecting to have two different classes which shape follow interfaces (even customized by generics)

The above proposal aim to bring the #3628 problem down to abstract classes. It does not allow injecting pure abstract interfaces. As stated at the beginning, by no mean Interfaces should have runtime-life as far as this is not strongly considered at ECMA International. DOM spec on itself have about a thousand interfaces and none come to exists at runtime.

While interfaces exist not at runtime, abstract classes do. And the suggest is to shift our concerns there.

@michaeljota
Copy link

One issue is that interfaces have no compile code. There is no way for they to be injected.

Besides, you want is a framework, like nest, or angular. You should check them out.

@SalathielGenese
Copy link
Author

SalathielGenese commented Dec 27, 2017

@michaeljota I know. Please check my answer above to @PFight.

Beside, I've used enough frameworks and am now writing mine. In fact, I want it lighter, faster, its components usable independently or packaged, to address the sole purpose of a framework (not a all-in-one solution), hybrid as simple as (let's say what, JSON), as powerful as (why not) spring... But in TS. I'll soon push the latest changes at @squall-io/magnitude-ts and @squall-io/magnitude under MIT.

Nevertheless, my suggest concerns TS alone. I just picked some examples to show what will become possible and how these TypeScript features will help to it.

@michaeljota
Copy link

I read them. I read the post actually, because it sounds interesting. But at the end, this is a language, and as such there is no reason to add a DI framework to it, but build a DI framework with it. I don't think there is a language that, as language, have a DI framework. There is C#, and the .NET Framework, but C# as is, does not allows DI per se.

@SalathielGenese
Copy link
Author

SalathielGenese commented Dec 27, 2017

@michaeljota, DI was a suitable use case to justify the need of these features and more... To show that some enforced constraints (which I still believe were not needed: I open this issue hoping in either implementation or explanation why this isn't possible) limited the possibilities of this (superset) language.

By no mean am I suggesting DI in typescript-core : to be sure, nowhere does the word DI appear in the summary of the proposals.

@michaeljota
Copy link

Literally in the title: TS Proposal : Interface DI, DI and metadata plus Constructor typing, and abstract class expression 😅

@SalathielGenese
Copy link
Author

SalathielGenese commented Dec 27, 2017

@michaeljota : finally got you. I'm updating

@SalathielGenese SalathielGenese changed the title TS Proposal : Interface DI, DI and metadata plus Constructor typing, and abstract class expression TS Proposal : abstract class expression, generic metadata, constructor typing, abstract class decorators, abstract members decorators Dec 27, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Jul 19, 2018

  1. Decorator support for abstract class

This is already supported today.

Decorator support for abstract members (of abstract classes) with the third parameter being something like AbstractedPropertyDescriptor or a fourth parameters as boolean (why not? let's be crazy this far)

The abstract method does not really exist at runtime. so there is nothing to decorate really. it is not clear what decorating an abstract method really means anyways.

Generate metadata that describe up to generic types

That is a runtime type system request, and it is outside the scope of the TS project. you might find the discussion in #3628 relevant.

Support of abstract class expression

Class expression decoration is tracked by #7342

Better constructor typing:
Either by adding a core generic type. Something like what is not valid yet now: type Constructor = typeof T; (because T is used here as a value !?)
Or by constraining that constructor interfaces build the same type, even when extending another constructor type

Tracked by #17572

Considered adding a name attribute to constructor types (i.e {new()...} and its interface avatar).

Function.prototype.name is already a JS feature. i believe you are looking for #1579

@mhegazy mhegazy added the Duplicate An existing issue was already created label Jul 19, 2018
@SalathielGenese
Copy link
Author

SalathielGenese commented Jul 29, 2018

@mhegazy ,

Thanks for the elaborated answer. Another project has been holding all my attention these past few months and I have not keep updating how TS is moving. Decorator support for abstract class is great improvement.

Thanks to TS team and contributors !

PS: This issue may now deserve to be closed !

@SalathielGenese
Copy link
Author

SalathielGenese commented Aug 9, 2018

Hi @mhegazy ,

Think back about what I had in mind when I suggested decorators for abstract members... I agree when you say

The abstract method does not really exist at runtime.

But as for...

It is not clear what decorating an abstract method really means anyways.

It means I want to attach some metadata about this.

A Use-Case

Let me borrow it from Java world where the following is possible... and might be meaningful when it comes to TypeScript:

public interface PetRepository extends JPARepository< Pet >
{

    @JQL("SELECT * FROM Pet WHERE id = { id } AND specie = '{ specie }' ")
    public ArrayList< Pet > complexQuery(id: Long, specie: String);

}

(Something to the likeness of what is shown on this web page)

Having decorators on an abstract member like this in TypeScript may let the decorator provide specific implementation, relying for instance, on a query builder.

@michaeljota
Copy link

@SalathielGenese I've been following this issue, as I though it may be useful for certain scenarios. However, what I understand, if that you would be able to decorate a interface or an abstract member, what should the Typescript compiler decorate at the end? The implementation of the member? Then, maybe what you mean is some kind of mixin pattern, but with decorators.

@SalathielGenese
Copy link
Author

SalathielGenese commented Aug 9, 2018

@michaeljota TypeScript MAY consider decorating abstract classes members and such decorators might help provide a proper implementation in the flow of IoC (Inversion of Control) / DI (Dependency Injection).

In the days I opened this proposal, I was working a tiny IoC/DI framework along with ORM features, almost as much powerful as what Spring Framework does in Java. I came about thinking that such features will enable some kinda magic in javascript through the TypeScript superset.

If one may have a generic interface abstract class member (as interfaces are not living at runtime) this way decorated, DI may complete the implementation and the magic happens.

I'll be continuing that project ASAP.

What should the Typescript compiler decorate at the end? The implementation of the member?

Well, just provide decorator with metadata as for implementation members - then decorator may cache those metadata and give them to the injector at DI time for implementation.


I tried to answer your question most clearly, let me know if some shadows are still there.

Kind Regards!

@michaeljota
Copy link

If one may have a generic interface abstract class member (as interfaces are not living at runtime) this way decorated, DI may complete the implementation and the magic happens.

Neither, nor interfaces, nor abstract class members, live at runtime. And what I ask was about that, giving the case as not interfaces or abstract members live at runtime, what should the compiler decorate?

@SalathielGenese
Copy link
Author

SalathielGenese commented Aug 11, 2018

Abstract class (abstract for TypeScript) do live at runtime (full class for JS) as shown on this playground page.

Again, this in-haste example shows that abstract class is complete at runtime.

Now, I'd expected I could decorate the abstract method.

@michaeljota
Copy link

Yes, abstract classes live at runtime, but you are talking about decorating abstract members, and they don't.

@SalathielGenese
Copy link
Author

but you are talking about decorating abstract members, and they don't.

I guess you mean TypeScript doesn't decorate abstract members... And that's exactly my feature request!

@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@javaxiu
Copy link

javaxiu commented Jun 21, 2019

i also want this feature to be supported. maybe we could give it a config in tsconfig.json just ignore the diagnostics error in editor ? after typescript compile, it (abstract methods and it's decorators ) would be delete, but using other compiler, it remain with some purpose in some other forms ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

6 participants