-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Arguments to callback prop of generic React component are not inferred #44596
Comments
I've looked into this some more. If I make the Playground link with relevant code import React from 'react';
type MakeRequired<T, K> = K extends keyof T ? Omit<T, K> & Required<Pick<T, K>> : T;
type Props<T extends React.ElementType> =
{ as: T } & MakeRequired<Omit<React.ComponentPropsWithoutRef<T>, 'as'>, 'onClick'>;
declare function Button<T extends React.ElementType>(props: Props<T>): JSX.Element;
// Using Button as React component (intended use case):
// OK: The type of `Button` is `function Button<"button">(props: Props<"button">): JSX.Element` in this instance.
<Button
as="button"
// OK: The type of `onClick` is `(JSX attribute) onClick: React.MouseEventHandler<HTMLButtonElement>`.
// OK: The type of `e` is `(parameter) e: React.MouseEvent<HTMLButtonElement, MouseEvent>`.
onClick={(e) => { console.log(e); }}
/>;
// Using Button as regular function (same issue):
// OK: The type of `Button` is `function Button<"button">(props: Props<"button">): JSX.Element` in this instance.
Button({
as: 'button',
// OK: The type of `onClick` is `(property) onClick: React.MouseEventHandler<HTMLButtonElement>`.
// OK: The type of `e` is `(parameter) e: React.MouseEvent<HTMLButtonElement, MouseEvent>`
onClick: (e) => { console.log(e); },
}); |
We can take a look earlier if someone can remove the |
After reading your comment I also searched through the issue tracker on DefinitelyTyped. There I found this issue for |
I've altered my original code sample to no longer include the Interestingly, if I comment line 19 ( Playground link with relevant line commented I can see how this might suggest that the issue lies within Edit: I see now that the |
Looking at this comment from earlier, I realised that it probably wasn't the "making the prop required" part of it all that made it work. I removed the - type MakeRequired<T, K> = K extends keyof T ? Omit<T, K> & Required<Pick<T, K>> : T;
+ type FixCallback<T, K> = K extends keyof T ? Omit<T, K> & Pick<T, K> : T; More interestingly, applying this fix to the import React from 'react';
type FixCallback<T, K> = K extends keyof T ? Omit<T, K> & Pick<T, K> : T;
type Props<T extends React.ElementType> =
{ as: T } & FixCallback<Omit<React.ComponentPropsWithoutRef<T>, 'as'>, 'onClick'>;
declare function Button<T extends React.ElementType>(props: Props<T>): JSX.Element;
// Using Button as React component (intended use case):
// OK: The type of `Button` is `function Button<"button">(props: Props<"button">): JSX.Element` in this instance.
<Button
as="button"
// OK: The type of `onClick` is `(JSX attribute) onClick: React.MouseEventHandler<HTMLButtonElement>`.
// OK: The type of `e` is `(parameter) e: React.MouseEvent<HTMLButtonElement, MouseEvent>`.
onClick={(e) => { console.log(e); }}
// NOT OK: The type of `e` is `any`.
onBlur={(e) => { console.log(e); }}
/>;
// Using Button as regular function (same issue):
// OK: The type of `Button` is `function Button<"button">(props: Props<"button">): JSX.Element` in this instance.
Button({
as: 'button',
// OK: The type of `onClick` is `(property) onClick: React.MouseEventHandler<HTMLButtonElement>`.
// OK: The type of `e` is `(parameter) e: React.MouseEvent<HTMLButtonElement, MouseEvent>`.
onClick: (e) => { console.log(e); },
// OK (Somehow?!): The type of `e` is `(parameter) e: React.FocusEvent<HTMLButtonElement>`.
onBlur: (e) => { console.log(e); },
}); |
@RyanCavanaugh I see that this is marked as backlog. A lot of people in the FE world use this prop inference pattern... would be good to at least target a milestone for fixing it. |
Here's the react free version @RyanCavanaugh asked for. Playground. (Uses react import just for the import React from "react";
declare const Identity:
<C extends keyof JSX.IntrinsicElements>
(props: { as?: C } & JSX.IntrinsicElements[C]) =>
JSX.Element
<Identity
as="input"
onChange={value => {
// value is any, expected string
}}
value="foo" />
Identity({
as: "input",
onChange: value => {
// value is string, expected string
},
value: "foo"
}) |
@RyanCavanaugh OK, I now have an actual interface Elements {
foo: { callback?: (value: number) => void };
bar: { callback?: (value: string) => void };
}
type Props<C extends keyof Elements> = { as?: C } & Elements[C];
declare function Test<C extends keyof Elements>(props: Props<C>): null;
<Test
as="bar"
callback={(value) => {
// expected: typeof value = string
// actual: typeof value = any (implicitly)
// NOT OK
}}
/>;
Test({
as: "bar",
callback: (value) => {
// expected: typeof value = string
// actual: typeof value = string
// OK
},
});
<Test<'bar'>
as="bar"
callback={(value) => {
// expected: typeof value = string
// actual: typeof value = string
// OK
}}
/>; It still seems so weird to me that the type of the callback is correct, but the type of the callback parameter is not: |
@Mesoptier thanks, that's a great repro |
#44596 (comment) works in 4.8.2, but the original code example by @Mesoptier still does not work. |
The issue in the original code comes with the Omit in the Props definition. |
I have a similar case and it's still not working: import React from 'react'
interface AsComponent<E extends React.ElementType> {
<As extends React.ElementType = E>(props: {as?: As} & React.ComponentProps<As>): JSX.Element | null
}
declare const Button: AsComponent<"button">
export const Test = () => {
return (
<div>
<Button onClick={e => {}}>Button</Button>
<Button onClick={e => {}} as="a">Link</Button>
</div>
)
} See the playground link. |
I fixed my issue like this: import React from 'react'
interface AsComponent<P, E extends React.ElementType<P>> {
(props: React.ComponentProps<E>): JSX.Element | null;
<As extends React.ElementType<P> = E>(props: { as?: As } & React.ComponentProps<As>): JSX.Element | null
}
declare const Button: AsComponent<{}, "button">
export const Test = () => {
return (
<div>
<Button onClick={e => { }}>Button</Button>
<Button onClick={e => { }} as="a">Link</Button>
</div>
)
} See the playground link. So the issue seems to be associated with type inference when some of the type parameters are omitted? |
I have been playing around with building a polymorphic box component which takes native html props as well as atomic class names from vanilla-extract sprinkles. <Box display="flex" borderWidth="thick" /> Which can also use as: <Box as="input" type="email" />
<Box as={Link} to="/home" /> // react-router-dom link I came up against the problem of event handlers not typing the event properly and like others have noted here it is related to using types like You may have noticed that type BoxProps<T extends ElementType = 'div'> =
ComponentPropsWithoutRef<T> & {
as?: T
ref?: Ref<ComponentPropsWithRef<T>['ref']> | null
} This example is largely based on the original types from the react-polymorphic-box lib. |
@gynekolog I'm not omitting any props, but I do want to set a default for the ElementType and apparently that causes the issue again. Here is your snippet with as an optional prop Link Again it knows the type of the onClick, but it says the event is any. What's strange is that this is the same thing: https://codesandbox.io/s/intelligent-marco-qdfwy4?file=/src/App.tsx And yet codesandbox doesn't complain. When I try to do the same thing in a CRA project, I get the implicit any error as well. |
The issue is because of |
I simplified your example and it still appears to work Link What's strange is that if I paste my example, it also seems to work... I'm not sure what's going on. |
I see. May be it's fixed in some recent version. |
The type of |
Seems like the problem occur just when using The reason why For now, using a conditional type on the omitted type seems to be enough to solve this: import React from "react";
// -------------------- Without Omit (OK) --------------------
type PropsWithoutOmit<As extends React.ElementType> = {
as?: As;
} & React.ComponentPropsWithoutRef<As>;
declare function TestWithoutOmit<As extends React.ElementType>(
props: PropsWithoutOmit<As>
): null;
<TestWithoutOmit
as="button"
onClick={(value) => {
// expected: typeof value = React.MouseEvent<HTMLButtonElement, MouseEvent>
// actual: typeof value = React.MouseEvent<HTMLButtonElement, MouseEvent>
// OK
}}
/>;
// -------------------- With Omit (NOT OK) --------------------
type PropsWithOmit<As extends React.ElementType> = {
as?: As;
} & Omit<React.ComponentPropsWithoutRef<As>, "a" | "b" | "c">;
declare function TestWithOmit<As extends React.ElementType>(
props: PropsWithOmit<As>
): null;
<TestWithOmit
as="button"
onClick={(value) => {
// expected: typeof value = React.MouseEvent<HTMLButtonElement, MouseEvent>
// actual: typeof value = any (implicitly)
// NOT OK
}}
/>;
// -------------------- Omit Workaround (OK) --------------------
type Fix<T> = T extends any ? T : never;
type PropsWorkaround<As extends React.ElementType> = {
as?: As;
} & Fix<Omit<React.ComponentPropsWithoutRef<As>, "a" | "b" | "c">>; // Fix here
declare function TestWorkaround<As extends React.ElementType>(
props: PropsWorkaround<As>
): null;
<TestWorkaround
as="button"
onClick={(value) => {
// expected: typeof value = React.MouseEvent<HTMLButtonElement, MouseEvent>
// actual: typeof value = React.MouseEvent<HTMLButtonElement, MouseEvent>
// OK
}}
/>; |
Bug Report
🔎 Search Terms
react, polymorphic components, generic components, type inference on function arguments
🕗 Version & Regression Information
⏯ Playground Link
Playground link with relevant code
💻 Code
🙁 Actual behavior
The argument
e
to the callback function provided toonClick
implicitly hasany
type.🙂 Expected behavior
I would expect the checker to know that the type of
e
can only beReact.MouseEvent<HTMLButtonElement, MouseEvent>
.The checker seems to know that the type of the
onClick
callback isReact.MouseEventHandler<HTMLButtonElement>
, which in turn resolves to(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
. So I would expect the checker to be able to use that information to determine the type ofe
/event
.The text was updated successfully, but these errors were encountered: