Typescript: What is the right way to use generic components with JSX?

Created on 21 Jul 2015  路  35Comments  路  Source: microsoft/TypeScript

Hello,

I have a generic component, e.g. class Select<T> extends React.Component<SelectProps<T>, any>. Is there any "right" way to use it from JSX?

It is impossible to write something like <Select<string> /> in JSX. The best thing that I've found is to write

let StringSelect: React.Component<SelectProps<string>, any> = Select;

And use StringSelect instead of Select<string>. Is there anything better?

Discussion JSTSX

Most helpful comment

Updated @RyanCavanaugh's example to be be something you can copy paste :rose:

/** Generic component */
type SelectProps<T> = { items: T[] }
class Select<T> extends React.Component<SelectProps<T>, any> { }

/** Specialization */
type StringSelect = new () => Select<string>;
const StringSelect = Select as StringSelect;

/** Usage */
const Form = ()=> <StringSelect items={['a','b']} />;

All 35 comments

I can't think of a better way to do it, though I'll try to come up with something more clever.

Note that your code isn't quite right (this is why you saw the crash in the other issue) -- it should be

let StringSelect: new() => React.Component<SelectProps<string>, any> = Select;

Your Select class is a constructor function, not an instance of it. It's probably worthwhile to make a type alias:

type ReactCtor<P, S> = new() => ReactComponent<P, S>;

let StringSelect: ReactCtor<SelectProps<string>, any> = Select;

@RyanCavanaugh yes, I see, you are right. I just could not go further and see error messages because of the compiler crash.

I think that generic components is not a common case, so maybe this workaround is quite enough right now (maybe it must be documented somewhere). But I will be glad to see a better way to do this.

Full working workaround for generic components:

import * as React from 'react';

interface JsxClass<P, S> extends React.Component<P, S> {
    render(): React.ReactElement<P>
}

interface Render<P> {
    render(): React.ReactElement<P>
}

interface ReactCtor<P, S> {
    new(props: P): JsxClass<P, S>;
}

interface Props<T> {
    val: T
}

class C<T> extends React.Component<Props<T>, {}> implements Render<Props<T>>  {
    constructor(props: Props<T>, context?: any)  {
        super(props)

        // this.state = /* ... */
    }

    render(): React.ReactElement<any> {
        return null
    }
}

let C1: ReactCtor<Props<number>, any> = C;
let a = <C1 val={1} />;

By the looks of it, this would require a single token of lookahead in the parser. Basically this:

  • If the next token is an angle brace (< Foo <), parse the production as a JSX element with a generic constructor.
  • Otherwise, parse it as a normal JSX element.

I don't have a lot of time to look into this, since college just started back up for me.

Instantiation of type aliases (#2559) would make things a bit easier:

class Select<T> extends React.Component<SelectProps<T>, any> { ... }

type StringSelect = Select<string>;

<StringSelect />
class Select<T> extends React.Component<SelectProps<T>, any> { ... }
type StringSelect = Select<string>;
<StringSelect />

Two things - first, generic type instantiations are allowed now. Second, this code is not correct -- Select<string> refers to an _instance_ of the Select class, but JSX element names refer to _constructor functions_ for those classes. The correct definition would be:

type StringSelect = new () => Select<string>;

Using the example above, when making use of StringSelect I always receive a "TS2304: Cannot find name 'StringSelect'" using Typescript 1.6.2. Example:

class Select<T> extends React.Component<SelectProps<T>, any> { ... }
type StringSelect = new () => Select<string>;
class Form extends React.Component<any,any> {
    render(): JSX.Element {
        return <StringSelect />;
    }
}

See gist of full example here: https://gist.github.com/be0453ed4a86c79da68e.git

Any ideas?

You need to write something like this. type doesn't create a value, so what you have would be a runtime error.

class Select<T> extends React.Component<SelectProps<T>, any> { ... }
type StringSelect = new () => Select<string>;
var StringSelect = <StringSelect>Select;
class Form extends React.Component<any,any> {
    render(): JSX.Element {
        return <StringSelect />;
    }
}

Updated @RyanCavanaugh's example to be be something you can copy paste :rose:

/** Generic component */
type SelectProps<T> = { items: T[] }
class Select<T> extends React.Component<SelectProps<T>, any> { }

/** Specialization */
type StringSelect = new () => Select<string>;
const StringSelect = Select as StringSelect;

/** Usage */
const Form = ()=> <StringSelect items={['a','b']} />;

Note: For whatever reason (not debugging it right now) type falls over if you use modules and import. interface still works:

/** Generic component */
type SelectProps<T> = { items: T[] }
class Select<T> extends React.Component<SelectProps<T>, any> { }

/** Specialization */
interface StringSelect { new (): Select<string> } ;
const StringSelect = Select as StringSelect;

/** Usage */
const Form = ()=> <StringSelect items={['a','b']} />;

Is there any practical reason why my 1-token lookahead wouldn't be feasible? (JS already requires a whole potential expression of lookahead with async arrow functions, which are implemented already when targeting ES6.)

@isiahmeadows feel free to log a separate suggestion for that. Might be worth looking in to

@RyanCavanaugh Done.

Unfortunately none of these work for me with 1.8.30.0.

Using:

export class MyTable<RowType> extends ReactComponent<TableProps<RowType>, TableState> {

With:

type MyT = new () => MyTable<ParentRow>;
const MyT = MyTable as MyT;

Results in:

Neither type 'typeof MyTable' nor type 'new () => MyTable' is assignable to the other.

However, a bigger hammer does compile and work:

const MyT = MyTable as any as MyT;

I believe no ReactCtor or type definition is needed. I do it like this:

Specialization:
const StringSelect = Select as new () => Select<String>;

Usage:
const Form = () => <StringSelect items={['a', 'b']} />

@fzzt resp:
const MyT = MyTable as new () => MyTable<ParentRow>;

Well I got it to work with everything defined in one spot with mock classes, so I guess it works in some cases, but not in others. I'll see if I can narrow down what the difference is...

It appears to be caused by static members on the class. If I delete all the static members, it works...

Alright here is what I'm seeing. With this [stripped down] code:

export abstract class ReactComponent<P, S> extends React.Component<P, S> {
    public abstract render(): JSX.Element;
    public patchState(patch, callback?: () => any): void {
        super.setState(patch as S, callback);
    }
}

export interface MyTableProps<DataType> {
    dataBuffer: Array<DataType>;
}

export interface MyTableState { }

export class MyTable<DataType> extends ReactComponent<MyTableProps<DataType>, MyTableState> {
    public static getFieldValue(fieldValue: any, type: any) {
        return fieldValue;
    }

    public constructor(props: MyTableProps<DataType>) {
        super(props);
    }

    public render(): JSX.Element {
        return null;
    }
}

And using this line:

const MyTableB = MyTable as new () => MyTable<any>;

I receive this error:

TS2352: Neither type 'typeof MyTable' nor type 'new () => MyTable' is assignable to the other.

Interestingly, if I remove the constructor, the error message changes a little (it still doesn't work). With this code:

export class MyTable<DataType> extends ReactComponent<MyTableProps<DataType>, MyTableState> {
    public static getFieldValue(fieldValue: any, type: any) {
        return fieldValue;
    }

    public render(): JSX.Element {
        return null;
    }
}

I get this message:

TS2352: Neither type 'typeof MyTable' nor type 'new () => MyTable' is assignable to the other. Type 'Component' is not assignable to type 'MyTable'. Property 'patchState' is missing in type 'Component'.

It almost sounds like the inheritance is pointing backwards. It could certainly be that I haven't set something up correctly. It all works (compiles and runs) fine with the as any shoved in there as I mentioned earlier, though.

In this particular case, I can refactor the statics out of there easily, however I would suspect that not being able to specify default props could mess up other components that really depend on them or classes that implement those interfaces from React.

const MyTableB = MyTable as new () => MyTable<any>;

This is a correct error, because MyTable's constructor function accepts one required parameter, and the anonymous type new () => ... only gets zero parameters.

This line should work:

const MyTableB = MyTable as new (props: MyTableProps<any>) => MyTable<any>;

There's a bug that was recently fixed that prevented the constructorless version from working.

this code work for me:

     render() {

            class ProjectTreeGrid extends TreeGrid<ProjectItem> {
            }

            return (
                <ProjectTreeGrid>
                    <TreeGridColumn caption="title"></TreeGridColumn>
                </ProjectTreeGrid>
            );
        }

@KostiaSA It's highly inefficient, though. Try lifting that out of the render function.

Adding another voice to @fzzzt's observation that the generic class having static members causes an error in when typing it.

Still having trouble with this... If my extending class has an additional protected method, it doesn't like the const line:

const TestSliderField = ContextualSliderField as new () => ContextualSliderField<any>;

Results in:

Error TS2352 Neither type 'typeof ContextualSliderField' nor type 'new () => ContextualSliderField' is assignable to the other.
Type 'Component' is not assignable to type 'ContextualSliderField'.
Property 'handleChange' is missing in type 'Component'.

It seems like the same issue I posted before but specifying the props in the constructor outputs the same error, as does private/protected and arrow/prototype definition, which I tried just for fun...

I'm not sure why Component<any, any> is there, why it would expect handleChange to be on that. ContextualSliderField<T> extends Component<SliderFieldProps, {}>, renders a SliderSwitch and adds a handleChange method which wraps the onChange callback from SliderField to add a context property.

This is with TypeScript 1.8.36.0.

what about:

const TestSliderField = ContextualSliderField as 
    new (props?: SliderFieldProps) => ContextualSliderField<any>;

I tried it with the props and receive the same error.

The cause seems to be the combination of generics and static properties, which I guess really is the same problem as before... This works:

export abstract class MyBase extends ReactComponent<MyBaseProps, {}> {
    public static defaultProps = {};
    protected abstract handleChange(newValue: string): void;
    public render() { return null; }
}
export interface MyBaseProps { }
export class MyFoo<P extends MyBaseProps> extends MyBase {
    protected handleChange(v) { }
}
export interface MyFooProps extends MyBaseProps { }
const CustomFoo = MyFoo as new () => MyFoo<MyFooProps>;

And this works:

export abstract class MyBase<T> extends ReactComponent<MyBaseProps, {}> {
    //public static defaultProps = {};
    protected abstract handleChange(newValue: string): void;
    public render() { return null; }
}
export interface MyBaseProps { }
export class MyFoo<P extends MyBaseProps> extends MyBase<P> {
    protected handleChange(v) { }
}
export interface MyFooProps extends MyBaseProps { }
const CustomFoo = MyFoo as new () => MyFoo<MyFooProps>;

But this doesn't:

export abstract class MyBase<T> extends ReactComponent<MyBaseProps, {}> {
    public static defaultProps = {};
    protected abstract handleChange(newValue: string): void;
    public render() { return null; }
}
export interface MyBaseProps { }
export class MyFoo<P extends MyBaseProps> extends MyBase<P> {
    protected handleChange(v) { }
}
export interface MyFooProps extends MyBaseProps { }
const CustomFoo = MyFoo as new () => MyFoo<MyFooProps>;

I guess this is basically a duplicate post, sorry about that. I didn't realize it was the same core issue until I removed defaultProps.

Unless I'm missing something, the type aliasing approach does only work outside of generic classes or functions, right?
E.g. when I want to do something like the following, I cannot get by with type aliases, can I?

type SelectProps<T> = { items: T[] }
class Select<T> extends React.Component<SelectProps<T>, any> { }

function Form<T>(items: T[]) {
  return <Select<T> items={items} />;
}

The only solution I found for now is not to use JSX syntax and directly code whatever it would have generated, e.g.:

function Form<T>(items: T[]) {
  return React.createElement(Select, { items=items });
}

Note: If TSC was able to infer the generic types from the parameters, it would not cause any parsing issues while enabling the following code to compile successfully:

function Form<T>(items: T[]) {
  return <Select items={items} />; // -> Select<T> type inferred through items property
}

I use this approach:

const StringSelect: new() => Select<string> = Select as any;
...
return <StringSelect items={items}/>;

And it solves the problem @avonwyss talked about:

function Form<T>(items: T[]) {
  const SpecificSelect: new() => Select<T> = Select as any;
  return <SpecificSelect items={items} />;
}

@mrThomasTeller This approach is not a good solution IMHO since it needs an any cast. For instance, the constructor parameters are not declared...

BTW, if you really want this feature, you might want to look at my suggestion in #6395, and if you're familiar with the internals (or willing to put the effort into it), it can be done. (The main issue is supposedly architectural, from what they said over there.)

Sorry to revive this, but on 2.x none of the above seems to work if you want an export that works as a type _and_ a value. ex:

type FeesUK = new () => Fees<FeesProps, any>;
const FeesUK: FeesUK = Fees as FeesUK;

This works for the component, ie you can now use FeesUK in a tsx expression or in React, but if assigning the component to variable it will fail, ex:

class Something extends React.Component<any, any> {
    private _reference: FeesUK;
    render() {
        return <FeesUK ref={(r) => this._reference = r} />;
    }
}

You will get an error when doing this._reference = r as the FeesUK type is actually a constructor, not the type. r will actually be of type Fees<FeesProps, any> but FeesUK is a function.

The only thing that seems to work to alleviate this is the following:

type FeesUK = Fees<FeesProps, any>;
type FeesUKCtor = new () => FeesUK;
const FeesUK: FeesUKCtor = Fees as FeesUKCtor;
export { FeesUK };

Thanks for adding it. Adding the release notes for future reference: http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html

To those of you using IntelliJ IDEA 2018.1.5 (current version) that does not support this syntax, a proxy or derived class will work.

eg proxy

export const FormValueListPicker = (props: Props<IFormValue<string>>) =>
  new ListPicker<IFormValue<string>>(props);

eg derived

export class FormValueListPicker extends ListPicker<IFormValue<string>> {}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

Zlatkovsky picture Zlatkovsky  路  3Comments

dlaberge picture dlaberge  路  3Comments

uber5001 picture uber5001  路  3Comments

wmaurer picture wmaurer  路  3Comments

blendsdk picture blendsdk  路  3Comments