Pampattitude
Pampattitude

Reputation: 456

Decorator for children of abstract generic Container class in Typescript

I'm having trouble with Typescript generics. I'm using Typescript 2.6.

The basic idea is that I want to create a MobX store that exposes a class decorator adding basic authentication check, and that decorator takes a class type derived from the abstract generic Container class (a React Router route container, for example).

Here is the code leading to the issue:

interface Newable<T, U = any> {
  new (...args: U[]): T;
}

function withAuth<TContainer extends Container<TProps, TState>, TProps extends Container.Props, TState>(ctor: Newable<TContainer>): Newable<TContainer> {
  return class WithAuth extends ctor {
    public async componentWillMount() {
      await this.props.stores.authentication.tryLoggingIn();
      super.componentWillMount();
    }

    public async componentWillUpdate() {
      await this.props.stores.authentication.tryLoggingIn();
      super.componentWillUpdate();
    }

    public render(): React.ReactNode {
      return super.render();
    }
  };
}

Both the TProps and TState generic parameters can be redefined by the derived class, as they may have more React props or a complex state.


The error messages I get from the Typescript compiler:

Type 'typeof WithAuth' is not assignable to type 'Newable<TContainer, any>'. Type 'WithAuth' is not assignable to type 'TContainer'. (line 6)
Base constructor return type 'TContainer' is not a class or interface type. (line 6)
Property 'props' does not exist on type 'WithAuth'. (line 8)
Property 'props' does not exist on type 'WithAuth'. (line 14)

Removing the return type of the withAuth function leads to this error:

Base constructor return type 'TContainer' is not a class or interface type.

Here is the relevant code for the Container class (simplified, it contains a little bit more stuff):

abstract class Container<T extends Container.Props = Container.Props, U extends React.ComponentState = {}> extends React.Component<T, U> {
  public abstract render(): React.ReactNode;
}

namespace Container {
  export type Props = React.Props<any>; // TODO: typings
}

What I don't get is, even using a non-generic class instead of Container leads to the

Base constructor return type 'TContainer' is not a class or interface type.

error. However, as you can see in the generic parameter declaration, TContainer IS DERIVED from a class. From what I get, this shouldn't be an issue.

So my question is, how can I create a class decorator to the Container<TProps, TState> that would add authentication checks to componentWillMount and componentWillUpdate?

Thanks a bunch!

Upvotes: 2

Views: 866

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249486

You are trying to use mixins, which are described here. The way it works is pretty particular, the class that will be augmented must be passed in as a type parameter, which can be constrained to extend the abstract class. Note that since we mandate the class will have a callable constructor, the class passed to withAuth can't be Container but rather a class that is derived from Container and implements the abstract methods already.

abstract class Container<T extends Container.Props = Container.Props, U extends React.ComponentState = {}> extends React.Component<T, U> {
    public abstract render(): React.ReactNode;
}

namespace Container {
    export type Props = {
        stores: any
    };
}

interface Newable<T, U = any> {
    new(...args: U[]): T;
}

function withAuth<TCtor extends Newable<Container<Container.Props, any>>>(ctor: TCtor) {
    return class WithAuth extends ctor {
        public async componentWillMount() {
            await this.props.stores.authentication.tryLoggingIn();
            this.componentWillMount();
        }

        public async componentWillUpdate() {
            await this.props.stores.authentication.tryLoggingIn();
            this.componentWillUpdate();
        }

        public render(): React.ReactNode {
            return this.render();
        }
    };
}

class NewComponent extends Container< Container.Props & { otherProp: string}, any> {
    public render(): React.ReactNode {
        // Actual implementation 
        throw new Error("Method not implemented.");
    }

}
const NewComponentWitAuth = withAuth(NewComponent);

let witAuth = <NewComponentWitAuth otherProp="" stores={null}  /> // props type is preserved

Upvotes: 2

Related Questions