简体   繁体   中英

Typescript Trait Implementation: how to let generic know it has been already been extended?

Hi maybe the title is not right, but I am already trying to specify the problem I am having with my limited knowledge. Please help to edit it if it's not right.

I am trying to create some trait function to mirror a class. Much more like PHP trait.


The reasons I am doing this are

  1. Trying use the same setting without writing it twice.

    like the minx func here https://www.typescriptlang.org/docs/handbook/mixins.html

    implementing class requires you to declare the target Class as undefined or etc first.

2 . Is not parent and child. It does not extend.


The function works well after compiling, but I just do not know how to resolve the TS error.

TS can not resolve that B already has A's method and property.

Code as below.

export const trait = ( Orig : any ) : ClassDecorator =>
  < T extends Function >( Tgt : T ) : void => {

    let _stats =
        Object
        .getOwnPropertyNames( Orig )
        .filter( prop =>
            prop != 'length'
            && prop != 'prototype'
            && prop != 'name'
        )
    ;

    for( let _stat of _stats )
      Object.defineProperty( 
        Tgt
        , _stat
        , Object.getOwnPropertyDescriptor( Orig , _stat ) || {}
      )


    let _insts =
        Object
        .getOwnPropertyNames( Orig.prototype )
        .filter( prop =>
            prop != 'constructor'
        ) 
    ;

    for( let _inst of _insts )
        Object.defineProperty( 
          Tgt.prototype
          , _inst
          , Object.getOwnPropertyDescriptor( Orig.prototype , _inst ) || {}
        )

  } ;



class A {

    propA = 1 ;

    static propB = 2 ;

} ;

trait( A ) 
class B {

    PropC = 3 ;

    static propD = 4 ;

} ;


console.log( B.propA ) ;

console.log( new B().PropB ) ;

I am getting errors like

Property 'propA' does not exist on type 'typeof B'. Did you mean 'propD'?

[update]

Thanks for @T ' help, after long try I found how the way to do this.

First at all, below the type checker. To merge static and prototypes.

type mir< C , O > = 
    { 
        new( ... agrs : any ) : 
            Inst< C >
            & { [ I in keyof InstanceType< O > ] ?: InstanceType< O >[ I ] ; }
    }
    & C
    & { [ I in keyof O ] ?: O[ I ] ; }
;

Secondly here is the way to implement it.

class orig {}

@trait( orig )
class tgt {}
// js will work here but ts error will ocurr.
// therefore extend the types as well.

const Tgt : mir< tgt , orig >  = tgt ;

So the trait will work in JS and if ur using TS requires to write more to configure types.

I'm not going to speak to the implementation, it does not work for me, but the question is on how to get the types to workout.

Decorators can't change the type of a class, this is by design. You can however use a function to transform the type of the class.

To merge the class properties we can use some conditional and mapped type magic. The solution:

type Constructor = new (...a: any[]) => any;
type Merge<TTrait extends Constructor, TTarget extends Constructor> =
    (new(...a: ConstructorParameters<TTarget>) => InstanceType<TTrait> & InstanceType<TTarget>) & Pick<TTarget, keyof TTarget> & Pick<TTrait, keyof TTrait> 

const trait = <TTrait extends Constructor>(Orig: TTrait) => 
    <TTarget extends Constructor>(Tgt: TTarget) : Merge<TTrait, TTarget> => {
        // perform patching 
        return Tgt as any; // assertion required
}



class A {
    propA = 1;
    static propB = 2;
};

const B = trait(A)(class {
    PropC = 3;
    static propD = 4;
});


console.log(B.propB);
console.log(B.propD);

console.log(new B().PropC);
console.log(new B().propA);


class C extends B {

}

A bit of explanation:

We need to take the two class types which will be captured in the TTrait and TTarget type parameters and merge them. To merge the types we will need a new constructor type that returns an intersection of the two instance types. We can get the instance types using the predefined conditional type InstanceType<T> . We can then intersect them to create the new instance type: InstanceType<TTrait> & InstanceType<TTarget>

The constructor also need to copy the parameters from TTarget . We can use ConstructorParameters to get the parameters from the target constructor and spread them back into the new constructor using tuples in rest parameters .

The final touch is to add back the statics of each class, removing the constructor in the original classes. We can do this using Pick which will pick all named properties but not constructor or function signatures.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM