Skip to content

Decorating react components with typing #7423

Closed

Description

On typescript 1.8, I'm trying to write better definition files for a lot of libraries out there, including my own. Many of these use the concept of a higher order component, by way of decorators.

Now, this is a stretch, but maybe there's a way to do it. I'll make the example simple, and borrow a modified version of #5887.

import * as React from 'react';
import {Component, ComponentClass, Props} from 'react';

export function Highlighted<T>(InputTemplate: ComponentClass<T>): ComponentClass<T> {
    return class extends Component<T, void> {

        render() {
            return (
                <div className={className}>
                    <InputTemplate foo={1} {...this.props}/>
                </div>
            );
        }
    }
}

/* some basic components */
interface MyInputProps {
    inputValue: string;
    foo: number;
}
class MyInput extends Component<MyInputProps, void> { };

interface MyLinkProps {
    linkAddress: string;
    foo: number;
}
class MyLink extends Component<MyLinkProps, void> { };

/* wrapped components */
const HighlightedInput = Highlighted(MyInput);
const HighlightedLink = Highlighted(MyLink);

/* usage example */
export class Form extends Component<any, void> {
    render() {
        return (
            <div>
                <HighlightedInput inputValue={"inputValue"} />
                <HighlightedLink linkAddress={"/home"} />
            </div>
        );
    }
}

Basically, it's almost the reverse of the linked issue. I want a component that expects foo. Imagine, say, in redux, it expects dispatch as a prop.

This example here will error out with:

1 client/tests.tsx|16 col 36 error| TS2339: Property 'foo' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<T, any>> & T'.
  2 client/tests.tsx|45 col 17 error| TS2324: Property 'foo' is missing in type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<MyInputProps, any>> & MyInputProps'. 
3 client/tests.tsx|46 col 17 error| TS2324: Property 'foo' is missing in type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<MyLinkProps, any>> & MyLinkProps'.
~

Meaning, we can't "inject" this property that comes from somewhere else other than when instantiating it.

Is this something Typescript could do? Am I missing a pattern? Or is this too much for the generics system?

Thanks again for the incredible work,

Silvio

Activity

  1. RyanCavanaugh commented on Mar 7, 2016

    @RyanCavanaugh
    Member

    It seems like what you want is this:

    import * as React from 'react';
    import {Component, ComponentClass, Props} from 'react';
    
    const className = 'not sure';
    
    export function Highlighted<T extends {foo?: number}>(InputTemplate: ComponentClass<T>): ComponentClass<T> {
        return class extends Component<T, void> {
    
            render() {
                return (
                    <div className={className}>
                        <InputTemplate foo={1} {...this.props}/>
                    </div>
                );
            }
        }
    }
    
    /* some basic components */
    interface MyInputProps {
        inputValue: string;
    }
    class MyInput extends Component<MyInputProps, void> { };
    
    interface MyLinkProps {
        linkAddress: string;
    }
    class MyLink extends Component<MyLinkProps, void> { };
    
    /* wrapped components */
    const HighlightedInput = Highlighted(MyInput);
    const HighlightedLink = Highlighted(MyLink);
    
    /* usage example */
    export class Form extends Component<any, void> {
        render() {
            return (
                <div>
                    <HighlightedInput inputValue={"inputValue"} />
                    <HighlightedLink linkAddress={"/home"} />
                </div>
            );
        }
    }

    There are definitely a few patterns where we don't have generics machinery in places to cover everything. I think the general thing you're going for here is essentially property currying where you could take some props type, provide values for a few of the properties, and synthesize a new type where those properties aren't present. There isn't a way to do that yet.

  2. silviogutierrez commented on Mar 7, 2016

    @silviogutierrez
    Author

    Thanks for getting back to me. The above won't really work because then if MyInput or MyLink at some point make use of this.props.foo, you'll get an error as it isn't available in MyInputProps and MyLinkProps. I suppose adding foo to these as an optional property is a decent workaround.

    I can see why this would be quite difficult, especially if you think in non-JSX world. Basically we're turning

    // The component
    class MyLink {
        constructor(public props: {foo: int}) {
        }
    }
    // As JSX
    <MyLink></MyLink> 
    
    // Internal call would look like:
    new MyLink({}); // errors, as it expects foo inside.
    
    // Internal call should be:
    function Wrapper() {
       return new MyLink({foo: 1});
    }
    return Wrapper();

    Does that seem like something generics could do? That is, in non JSX-land? I doubt it, but it's worth asking.

    Thanks again for getting back to me.

    Silvio

  3. RyanCavanaugh commented on Mar 7, 2016

    @RyanCavanaugh
    Member

    Right, there's no way to represent this today.

    #6895 is probably the closest solution. I think that would actually solve this use case very cleanly

  4. silviogutierrez commented on Mar 8, 2016

    @silviogutierrez
    Author

    That looks exactly right! Closing this ticket. That looks more proposal-like. Very elegant, might I add.

    Thanks.

  5. stiofand commented on Nov 22, 2017

    @stiofand

    There is currently no solution for this without a convoluted work around, TS and ReactRedux teams, dont communicate, so nothing gets done. Moved away from Rreact Ts because of this

  6. locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions