1

I'm trying to type the following component:

type Props<Data> = {
  data: Data[]
  render: ({ value }: { value: Data }) => React.ReactElement
}

const List: React.FC<Props> = ({data, render}) => {…}

The problem I'm having is that I want Data to be generic, whatever you pass in within an array I want to strongly type the render to know value as type Data. The problem is I'm not sure how to use this. Ideally I want to do the following:

<List data={[1,2,3]} render={(value) => <div>{value + 2}</div>}/>

and be done, in this case render would know that value is a number. Is this possible?

3
  • you would need to pass the type through. const List: React.FC<Props<MyTypeHere>> Commented Jul 25, 2019 at 19:00
  • ok, but now I get Cannot find name 'MyTypeHere'. Commented Jul 25, 2019 at 19:15
  • 4
    Well.. that's because it was an example. You're passing a generic type Data to your interface. You need to pass whatever that is through in the component type definition Commented Jul 25, 2019 at 19:21

2 Answers 2

3

If you are trying to pass an array of numbers (extrapolated from data={1,2,3}). then you just need to pass number as the generic type to your Props. Theres more mapping you need to do to get the types to match correctly.

Functional Component Way

export interface ListProps<T = unknown> {
  data: T[];
  render: (item: T) => React.ReactElement;
}

export const GenericListItems = <T extends any>(): React.FC<ListProps<T>> => ({
  data,
  render
}): any => {
  return data.map(render);
};

Strongly typing your list would look like

const StringList = GenericListItems<string>();
const NumberList = GenericListItems<number>();

Component Class Way

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

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

interface ListProps<T> {
  data: T[];
  render: (value: T) => any;
}

class List<T> extends React.Component<ListProps<T>, {}> {
  render(): React.ReactElement<any>[] {
    return this.props.data.map(this.props.render);
  }
}

Strongly typing your list would look like

const NumberList: GenericComponentType<ListProps<number>, {}> = List;
const StringList: GenericComponentType<ListProps<string>, {}> = List;

Both of these (Component Class or Functional Component) instances can be used the same way.

<NumberList
  data={[1, 2, 3]}
  render={value => <div>{value + 2}</div>}
/>
<StringList
  data={["I", "Render", "Strings"]}
  render={value => <div>{value}</div>}
/>
Sign up to request clarification or add additional context in comments.

1 Comment

is there a way to do this so I can describe the type of data when using the component? Or is it required to do something like const NumberList: GenericComponentType<ListProps<number>, {}> = List;?
0

If you need Data to be a generic parameter, then you can declare it as such to your component definition. So something like class List<Data> extends React.Component<ListProps<Data>> or function List<Data>(props: ListProps<Data>) and let typescript do the rest.

Here's an complete code example:

import React from "react";

interface ListProps<Data> {
  data: Data[];
  render: (item: Data) => React.ReactNode;
}

function List<Data>({ data, render }: ListProps<Data>) {
  return <div>{data.map(render)}</div>;
}

const element1 = <List data={[1.234, 2.345, 3.456]} render={v => <span>{v.toFixed(2)}</span>} />;

const element2 = (
  <List data={["abc", "bca", "cab"]} render={v => <span>{v.substring(0, 1)}</span>} />
);

EDIT

To force data to be of a specific type, you can pass the generic parameter to List like so:

const element1 = (
  <List<number>
    // Typescript will complain:
    // Type 'string' is not assignable to type 'number'.ts(2322)
    data={["1.234", 2.345, 3.456]}
    render={v => <span>{v.toFixed(2)}</span>}
  />
);

In this last example, if you remove the generic paramter, you'll get another error because Data would be infered to be number | string:

const element1 = (
  <List
    data={["1.234", 2.345, 3.456]}
    // Typescript will complain:
    //Property 'toFixed' does not exist on type 'ReactText'.
    //  Property 'toFixed' does not exist on type 'string'.ts(2339)
    // and `v` is infered to be (parameter) v: string | number
    render={v => <span>{v.toFixed(2)}</span>}
  />
);

6 Comments

You aren't strongly typing the data. If I put quotes around one of the numbers for instance, typescript doesn't complain and the code would blow up because .toFixed is not a function on a string.
@JohnRuddell are you dead certain about this? Because doing const element1 = <List data={["1.234", 2.345, 3.456]} render={v => <span>{v.toFixed(2)}</span>} />; totally gives me a Property 'toFixed' does not exist on type 'ReactText'. Property 'toFixed' does not exist on type 'string'. because the typesystem infers (parameter) v: string | number.
Except you never define the type to be string | number
I seriously don't understand what's the matter here. Are you aware of type inference or do you just assume that the code doesn't work?
lol, yes i am aware. except the OP wants to strongly type it so that you can for instance complain if you have different data types. What I'm pointing out is that currently the only reason why typescript would complain is because of how you are using the data rather than limiting the actual data type itself. Strongly typing the field rather than inferring it. if you weren't calling toFixed but rather an operation that both data types would accept then you wouldn't have a compile error. But this can have untoward effects in terms of behavior on a specific datatype
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.