Approaching the `as` Prop with IIMTs and Generics
Our goal is to be able to properly type the props we get from using as
in this Wrapper
component:
export const Wrapper = (props: any) => { const Comp = props.as; return <Comp {...(props as any)}></Comp>;};
As mentioned, there are two different ways to solve this problem.
Transcript
00:00 Okay, let's give this a go. The first way I'm going to tackle this is by first of all, looking at jsx.intrinsicElements and seeing if I can basically create a massive union out of it to do what I want in terms of the props. Could you imagine this? I kind of want a prop shape that looks like this.
00:16 So like wrapper shape, let's say, and I want it to kind of be a discriminated union where we have as, let's say, input and then and component props input, right? And I want one of these
00:32 for basically every single one of jsx.intrinsicElements. So if I have another one like this and I just pull that in there, let's say, and I have a button instead, then I kind of want to just create a massive shape that will handle all of the cases for me. And so if I take props here
00:50 and I've just put it in for wrapper shape there, then it's going to do a little bit for me here. So I've got my as, I want div, don't I? So let's just try that. Yeah, and now it actually works, right? So this is actually like one shape, one way that you can kind of create like a budget version of an as prop actually, but we want to kind of transform it
01:09 from the source of truth, which is jsx.intrinsicElements. So let me just comment that out for now. And let me just say wrapper props here as well. Now this wrapper props, let's just sort of walk through this. So wrapper props, what we're going to be doing here is probably creating a discriminated union
01:30 based on jsx.intrinsicElements. So if I just say, let's just say key in jsx.intrinsicElements. And if we just as a little reminder, this is a type here that basically has all of the stuff here along with all of its props.
01:47 So these are the props that A takes in React, which is useful. So let me just pull that in. So I now have, this is the element in jsx.intrinsicElements. We're inside a mapped type here basically. And the weapon that I would like to use here
02:05 is an immediately indexed map type. What I can do here is I can say as elements like this. And what will happen is when I transform this, so in fact, I'm going to go element in key of jsx.intrinsicElements. Now, as you can see, we have A as A, abbreviation as abbreviation.
02:24 You can see we're starting to get somewhere 166 more. I can actually index into this with key of jsx.intrinsicElements. Stopped being able to speak there for a second. And now what we end up with is kind of pretty similar to what we have before this shape there,
02:43 where we have key of jsx.intrinsicElements. And we've got as A, as abbreviation, as address. And that is going right into the wrapper props and it's going as all of this. So we're now getting autocomplete here. We're just not getting the other elements in there. So let's just add those in. So we've got the elements, which is div, button, et cetera.
03:02 Now we can just say and component props elements. So here we go. And I can already feel my ID slowing down a bit. So this is now working. This wrapper props here, if we take a look at it, we've got as symbol and SVG props, this as object and this,
03:21 and it's instantiating about 166 different types all at once. It's a little bit much. Okay. So our wrapper now it's strongly typed. So I can like put in a, I don't know, article here, for instance, is this going to work? Or maybe audio is better.
03:41 And now I can add an onClick. I can feel my ID just being real, real slow. And now E is going to be a type of React.mouseEvent HTML audio element. It's working, but there's a better way. Okay. As you can probably tell from my reluctance to do this, I'm tempted even to comment this out
04:00 just so the rest of this actually runs. Like the reason this is so slow is because it's instantiating everything it could possibly need eagerly. So inside wrapper props is a massive union type. And it may be that we just don't need to instantiate all of those elements.
04:18 All we need to do is when we call wrapper, you know, when we pass it some props, we need to pass it an as prop. And that as prop is going to describe the one that we want. And then we just instantiate those props. We don't need to instantiate all 166 more. So let's try another solution here
04:35 by adding in a type argument to this wrapper. We'll say as, and we'll say this extends key of jsx.intrinsic elements. Now we can add that onto the props and we can say as is T as here. So this is starting to go pretty well. We've got now button working nicely
04:53 and we've got auto-complete here. And then what we do is we just say, okay, in addition to the T as, we can say react.component props and then pass in T as here. And this now works. At least it works on the outside of the function. We'll get to the inside in a second.
05:13 So now this button here, we basically like load up this and we say, okay, if this is an A, then this is going to be react.mouseEvent HTML A elements. Really, really nice. So we've got it working. That's really, really cool. But inside of the component, it's a little bit sketchier.
05:32 This one, it's got a kind of crazy error here where it's jsx element type comp does not have any construct or call signatures. Now this error is a little bit hard to read, but it's essentially saying, okay, this component here is a little bit too complex for me to read now,
05:49 now that it's got all of this K of jsx.intrinsic elements. So we're actually just going to slap another as on it and say as string here. Now what this does is it basically says, okay, this resolves down into a string eventually. And you can see that it's almost there, right?
06:05 It's like T as all this and string are undefined. It's very, very close to string, but it is actually like we'll get there eventually. So yeah, this looks pretty ugly in terms of like the internal function definition here. We're annotating these props as any, just checking if we can actually do that. I think we might be able to get away with not doing,
06:23 oh no, we can't, no way. So this wrapper then, we've got a generic type, which just sort of a type argument, which sits on the front and basically says, okay, we capture the type of as. Once we've captured that, and only then do we then go deeper and compute all of the props to do with that,
06:42 React.ComponentProps. So this is a much more performance way to handle it. Instead of instantiating everything upfront using the int that we had before, we now just say, okay, we've got the component props that we need for the as that we're specifying. And this, I would say, is the most naive version of an as that we can get.
07:02 But as you can see, it's starting to work, which is good.