Danny Verpoort
Danny Verpoort

Reputation: 55

Extend React Abstract Component in Typescript

I'm creating a library with presentations in React. Each presentation component must have at least a few attributes set so I created a typescript file which can be extended to create a presentation:

import React from "react";

abstract class _Presentation<P,S> extends React.Component{
    static title: string
    static description: string
    static date: Date
    static uri: string
}

export type PresentationType = typeof _Presentation

export function Presentation<P,S>(mandatory: {
    title: string
    description: string
    date: Date
    uri: string
}) {
    return class extends _Presentation<P, S> {
        static title = mandatory.title
        static description = mandatory.description
        static date = mandatory.date
        static uri = mandatory.uri
    }
}

When I try to implement a presentation everything goes well and I can create a component like so:

import React from 'react';

class SomePresentation extends Presentation({
    title: "My presentation's title",
    description: "My presentation's description",
    date: new Date(2021, 1, 22, 15, 30, 0),
    uri: "/presentation-slug"
}) {

    render = () => (
        <div>
          Test
        </div>
    );
}

export default SomePresentation;

Everything starts coming apart when I start using states. There seems to be something amiss in my abstract's components typing. I can't figure out how to properly make this work with the right types.

import React from 'react';

class SomePresentation extends Presentation<{}, {
    width: number,
    height: number
}>({
    title: "My presentation's title",
    description: "My presentation's description",
    date: new Date(2021, 1, 22, 15, 30, 0),
    uri: "/presentation-slug"
}) {

    constructor(props: any) {
        super(props);
        this.state = {width: 0, height: 0}
        this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
    }

    componentDidMount() {
        this.updateWindowDimensions();
        window.addEventListener('resize', this.updateWindowDimensions);
    }

    componentWillUnmount() {
        window.removeEventListener('resize', this.updateWindowDimensions);
    }

    updateWindowDimensions() {
        this.setState({width: window.innerWidth, height: window.innerHeight});
    }

    render = () => (
        <div style={{width: this.state.width, height:this.state.height}}>
           
        </div>
    );
}

export default SomePresentation;

Where I'm using this.state.width I receive the following error:

Property 'width' does not exist on type 'Readonly<{}>'

I've already spent several hours search on how to correctly use the types here but I'm at a lost. Who can point me in the right direction?


EDIT: To further clarify I'm using these static variables to load a list of presentations on my index pages. I have an index.tsx which exports all presentation components. Then on the main page I have the following code:

import * as presentations from './presentations/index'
...
const presentationClasses = Object.values(presentations);
...

{presentationClasses.map((presentation, index) =>
    <Route key={presentation.uri} path={presentation.uri}>
        {React.createElement(presentation)}
    </Route>
)}

Upvotes: 1

Views: 1724

Answers (1)

TypeScript uses compiler and when the compiler generates JavaScript, all abstract properties are removed. All type aliases and interfaces. So when you have an abstract class with all abstract properties, it's slow down, TS generates empty class/function. So use interfaces, look at this example:

class A {
    inherited = 1;
}
interface B {
    abstract: string;
}
class C extends A implements B {
    abstract = "Yes, I implement"
}

This much better, compile result:

"use strict";
class A {
    constructor() {
        this.inherited = 1;
    }
}
class C extends A {
    constructor() {
        super(...arguments);
        this.abstract = "Yes, I implement";
    }
}

So no empty class. I fully recommend this:

interface _PresentatonBase {
    title: string;
    description: string;
    date: Date;
    uri: string;
}

interface _Presentation<P, S> extends React.ComponentClass<P, S>, _PresentatonBase {}

function Presentation<S, P = {}>(mandatory: _PresentatonBase, state: S): _Presentation<P, S> {
    return class SyntheticPresentation extends React.Component<P, S> {
        state = state;
        static title = mandatory.title;
        static description = mandatory.description;
        static date = mandatory.date;
        static uri = mandatory.uri;
    };
}

class SomePresentation extends Presentation({
    date: new Date(),
    description: "",
    title: "",
    uri: ""
}, {
    width: 0,
    height: 0
}) {
    constructor(props: any) {
        super(props);
        this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
    }

    componentDidMount() {
        this.updateWindowDimensions();
        window.addEventListener('resize', this.updateWindowDimensions);
    }

    componentWillUnmount() {
        window.removeEventListener('resize', this.updateWindowDimensions);
    }

    updateWindowDimensions() {
        this.setState({width: window.innerWidth, height: window.innerHeight});
    }

    render() {
        return <div style={{width: this.state.width, height: this.state.height}}>

        </div>;
    }
}

Upvotes: 1

Related Questions