0

I'm attempting to write a universal wrapper component that accepts an as prop which indicates the element to render and also requires any props that the element being rendered requires. With help from this answer, I have a working example:

import React, { ComponentType, ElementType, ComponentProps } from "react"

type WrapperProps<P = {}> = {
    as: ComponentType<P> | ElementType
} & P

const Wrapper = <P = {}>({ as: Element, ...rest }: WrapperProps<P>) => <Element {...rest} />

const Link = ({ href }: { href: string }) => <a href={href}>Click Me</a>

const Test = () => <Wrapper as={Link} /> // Should error as `href` is missing.
const Test2 = () => <Wrapper as={Link} to='/' /> // Should error as `to` is not a valid prop.
const Test3 = () => <Wrapper as={Link} href='/' /> // Should compile.

I'm unable to wrap my head around why the Wrapper component requires it's own generic type P. In other words, why this wouldn't work:

const Wrapper = ({ as: Element, ...rest }: WrapperProps) => <Element {...rest} />

The type WrapperProps already defines P as a generic which defaults to an object. Why must I redefine that when typing Wrapper? Is React/TS somehow passing a value for this variable automatically when I call Wrapper and if not, what extra information does that compiler get from me duplicating the declaration that P defaults to an empty object?

Update

I wanted to provide some additional context. I understand that if I were to define this type inline, I would need to add the generic to Wrapper as obviously it needs to come from somewhere:

const Wrapper = <P = {}>({ as: Element, ...rest }: {
    as: ComponentType<P> | ElementType
} & P) => <Element {...rest} />

I also understand that I were passing an explicit type for the generic when calling Wrapper then Wrapper would need it's own generic to be able to "pass" that to WrapperProps:

<Wrapper<ComponentProps<typeof Link>> as={Link} href='/' /> 

What I'm missing is that since I am not passing any type to Wrapper why do I need it to accept a generic? In other words, the type WrapperProps already knows that the type P defaults to an empty object, why does the function Wrapper need to redeclare this?

It seems redundant for Wrapper to set P to an empty object and then pass that type to WrapperProps as WrapperProps could have just set it's own P to an empty object.

1
  • You don't have to set a default generic for Wrapper, but then it would be a required generic, which might be frustrating to use. Commented Aug 25, 2022 at 19:59

1 Answer 1

1

Wrapper is a function that must take an argument. The argument is of type WrapperProps, which is generic.

If you didn't make Wrapper generic, then WrapperProps would either need to declare its generic type explicitly or else it would fall back to its default:

// WrapperProps is implicitly WrapperProps<{}>
const Wrapper = ({ as: Element, ...rest }: WrapperProps) => <Element {...rest} />

const Wrapper = ({ as: Element, ...rest }: WrapperProps<Link>) => <Element {...rest} />

In this sense, it's no different than a chain of two function, where you want to pass a value to the first so that it can pass it to the second:

function foo(p = {}) { return p; }
function bar(p = {}) { return foo(p); }

bar({ baz: "value" }); // returns { baz: "value" }
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks! Isn't the value I'm passing P = {} already the same as the implicit value of P in WrapperProps? Your function analogy makes sense but only when bar is called with a non-empty object. E.g. bar() is identical to bar({}).
@user1032752 - Wrapper is being assigned a generic anonymous function. For example, given const foo = <T>(a:T) => T, then foo is a function that will return the same type as the argument it's passed. E.g. foo(123) // returns number, foo("apple") // returns string
I appreciate your help and while I understand your examples, I'm failing to understand how it applies to my question. I've added some more context to my question if you don't mind taking a look.

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.