1

I have a component that accepts a generic type parameter TChildProps:

type GenericFieldHTMLAttributes =
    | InputHTMLAttributes<HTMLInputElement>
    | SelectHTMLAttributes<HTMLSelectElement>
    | TextareaHTMLAttributes<HTMLTextAreaElement>

interface FieldProps {
    name: string
    label?: string | React.ReactNode
    component?: string | React.ComponentType
    initialValue?: any
    children?: React.ReactNode | FieldRenderFunction
}

class Field<TChildProps = GenericFieldHTMLAttributes> extends React.PureComponent<FieldProps & TChildProps> {
    ...
}

When I use this component, I would expect it to prevent me from passing in unrecognized props, for example:

render() {
    return (
        <Form onSubmit={this.save}>
            <Field foo="test" label="email" name="email" type="email" component={TextField} />
        </Form>
    )
}

Surprisingly, the above code compiles without even any warnings, despite the fact that the foo prop is not defined anywhere. I tried simplifying the example and got the same result:

class Field<TChildProps = {}> extends React.PureComponent<FieldProps & TChildProps> {
    ...
}

// this still compiles without errors
<Field foo="test" label="email" name="email" type="email" component={TextField} />

Is TypeScript functioning the way it's supposed to here according to the type definition for React.PureComponent (by the way I tested it on React.Component and got the same result), or is this a bug?

1 Answer 1

1

Original Answer

Because <TChildProps = {}> means that the default value of TChildProps is {} if the consumer does not assigns any value to TChilProps. So TChildProps can actually be anything if the consumer decides to pass value to it. That's why typescript allows any props passed to Field. What you want to do is to enforce the type of TChildProps using extends keyword. So try class Field<TChildProps extends GenericFieldHTMLAttributes> instead. As a result, the consumer needs to pass TChildProps as GenericFieldHTMLAttributes type. You can find more information on https://www.typescriptlang.org/docs/handbook/generics.html.

UPDATE

Here's the simplified situation (UPDATE2: I tend to think this is either a bug or a missing feature, here's why)

This Animal class below accepting food as generics reflects React.Component class that accepts props as generics.

class Animal<F> {
  eat(food: F) {
    return;
  }
}

And the Lion class below extending Animal reflects your Field class which extends React.Component

class Lion<F = {pizza: string}> extends Animal<F> {

}

And when the consumers consume it, if they do not specify what food a lion should eat, the lion can eat anything like so we should expect the lion to eat only pizza

const lion1 = new Lion();
lion1.eat(""); // Argument of  type '""' is not assignable to parameter of type '{pizza: string}'

However this type check does not happen in case of tsx.

The workaround is to "alias both the type and interface to a non-generic specialization."

const TextField = Field as { new (): Field<{value: string}>};

<TextField foo="test" label="email" name="email" type="email" value="asdf"/> // error: Property 'foo' does not exist

These two links should give you more info on how to use generic components as of now.

https://github.com/Microsoft/TypeScript/issues/3960 https://basarat.gitbooks.io/typescript/docs/types/generics.html#generics-in-tsx

And apparently they working on this.

UPDATE April 2018

This should be possible in Typescript 2.9 as seen in https://github.com/Microsoft/TypeScript/pull/22415.

Sign up to request clarification or add additional context in comments.

4 Comments

Hmm, thanks for your answer and the info on extends, but I just wanted GenericFieldHTMLAttributes to be a default; I want to give the consumer the option to pass a completely different type if they want (e.g. the props for a datepicker component). I will play around with extends to see if I can come up with a solution...
Now that I think about it more, this answer doesn't make sense. In my example, I'm not passing anything into the generic TChildProps type, so it should just be using the default type GenericFieldHTMLAttributes or, in my simpler test, just an empty object - both of these definitely do not include a property called foo.
Thanks for the additional info, very helpful! (Actually I consider your update the real answer; the part at the top could be labeled 'Original Answer', but that's just a minor suggestion.) It seems like there might be an additional issue here besides the inconvenient workaround for using generics in TSX...namely that TSX is ignoring the default parameter value of the generic, which I think should work regardless. I might file a bug report for this in the TypeScript repo if it doesn't exist already.
FYI I think the basarat.gitbooks.io page might have been reorganized...in any case here's a current link to the specific issue at hand: basarat.gitbooks.io/typescript/docs/types/…

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.