Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is the right way to use generic components with JSX? #3960

Closed
s-panferov opened this issue Jul 21, 2015 · 35 comments
Closed

What is the right way to use generic components with JSX? #3960

s-panferov opened this issue Jul 21, 2015 · 35 comments
Labels
Discussion Issues which may not have code impact Domain: JSX/TSX Relates to the JSX parser and emitter

Comments

@s-panferov
Copy link

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?

@s-panferov s-panferov changed the title What is the right way to use generic components with JSX What is the right way to use generic components with JSX? Jul 21, 2015
@RyanCavanaugh
Copy link
Member

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 RyanCavanaugh added the Discussion Issues which may not have code impact label Jul 21, 2015
@s-panferov
Copy link
Author

@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.

@s-panferov
Copy link
Author

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} />;

@dead-claudia
Copy link

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.

@smrq
Copy link

smrq commented Sep 18, 2015

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 />

@RyanCavanaugh
Copy link
Member

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>;

@evilangelist
Copy link

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?

@RyanCavanaugh
Copy link
Member

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 />;
    }
}

@basarat
Copy link
Contributor

basarat commented Dec 17, 2015

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

/** 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']} />;

@basarat
Copy link
Contributor

basarat commented Dec 17, 2015

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']} />;

@dead-claudia
Copy link

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.)

@RyanCavanaugh
Copy link
Member

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

@dead-claudia
Copy link

@RyanCavanaugh Done.

@josh-endries
Copy link

josh-endries commented May 27, 2016

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.

@josh-endries
Copy link

However, a bigger hammer does compile and work:

const MyT = MyTable as any as MyT;

@Liero
Copy link

Liero commented Jun 1, 2016

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>;

@josh-endries
Copy link

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...

@josh-endries
Copy link

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

@josh-endries
Copy link

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<any, any>' is not assignable to type 'MyTable'. Property 'patchState' is missing in type 'Component<any, any>'.

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.

@RyanCavanaugh
Copy link
Member

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.

@KostiaSA
Copy link

this code work for me:

     render() {

            class ProjectTreeGrid extends TreeGrid<ProjectItem> {
            }

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

@dead-claudia
Copy link

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

@fraserharris
Copy link

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

@josh-endries
Copy link

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<any, any>' is not assignable to type 'ContextualSliderField'.
Property 'handleChange' is missing in type 'Component<any, any>'.

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.

@Liero
Copy link

Liero commented Oct 13, 2016

what about:

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

@josh-endries
Copy link

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

@josh-endries
Copy link

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.

@avonwyss
Copy link

avonwyss commented Nov 16, 2016

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
}

@mrThomasTeller
Copy link

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} />;
}

@avonwyss
Copy link

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

@dead-claudia
Copy link

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.)

@caesay
Copy link

caesay commented May 15, 2017

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 };

@mohsen1
Copy link
Contributor

mohsen1 commented Mar 29, 2018

JSX generics will land in 2.9

#22415

@gitsupersmecher
Copy link

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

@demoran23
Copy link

demoran23 commented Jul 12, 2018

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>> {}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Discussion Issues which may not have code impact Domain: JSX/TSX Relates to the JSX parser and emitter
Projects
None yet
Development

No branches or pull requests