Two Approaches to Defaults for the `as` Prop
Let's start by looking at an approach that nearly works.
Here's the Link
component we started with:
export const Link = <TAs extends ElementType>(
props: {
as: TAs;
} & React.ComponentPropsWithoutRef<TAs>,
) => {
const { as: Comp = "a", ...rest } = props;
return <Comp {
Transcript
00:00 Okay, let's look first at the approach that nearly works. The thing that you might think here that should work is you should be able to specify T as if you don't pass a T as, like let's actually just instantiate this as like, just call it right with a link. Now, you might think that this link here,
00:18 currently it's being inferred as element type, right? In the little slot there. So, because that, because we're not passing an as, that's what's going to be inferred as the default because that's kind of what it's constrained to. So, if we say as, let's say as a, for instance here, now it's being inferred as a, just there.
00:37 So, if we don't pass an as, it makes sense that we should default it to a here. And now this link, here we go, it's now being defaulted to a, which is nice. And we no longer need to pass the as by default. So, let's see if this works now. It seems that everything is passing,
00:56 which is really, really nice. So, actually all of our errors disappear here. Link is being inferred as a, we've got our a there, and now we should, yet we get all of the props to do with that a there. Sorry, I keep saying the word a and it's freaking me out. So, we've got this e now, and this e is typed as HTML anchor elements,
01:15 mouse event, which is beautiful. Except we lose something. And the something that we lose is we lose this autocomplete here. For some reason, TypeScript seems to think that a now is the only possibility. This might change in a future version of TypeScript, but given that this is the one we're working with,
01:34 it's kind of annoying, and it means that we have to be a bit more creative about our solution. Now, the solution that I found to this problem is that you can use t as here. We cannot use a default type here. Can't use a default type. And if we can't use a default type, then we're in a little bit of trouble.
01:53 Because when we call this link here, this is going to be inferred as basically element type. And this means that you can basically pass any props to it, or it certainly won't resolve properly. So on click, for instance, e any void, it's just freaking out on us here. No good. So, the solution I found is that essentially
02:13 we want to be thinking about this thing here. The thing that we pass to component props without ref. Because if we pass this just t as, it's not really going to work too well. But actually, if we do something like this, where we use a conditional type, inside here, what we want to do is we want to basically say,
02:32 if t as is the default, don't basically pass it a instead. But if t as has a value that isn't the default, we want to just pass it that. So we basically want to check if this is being inferred
02:53 as its base type, as element type. And the way that you can do that in TypeScript is with a conditional type. So we can say, first of all, we want to check if t as is the default. So let's just say string extends string. Let's just say a or t as, because those are the things that we kind of care about here.
03:13 So if we have this now, we now have a conditional here that's checking if string extends string. If you can pass string to string, then pass it a. So this is actually working for this one here. Now, e is inferred properly as an a there, except all of this stuff isn't working because it's no longer letting you specify like any other type there.
03:34 So we need to figure out this. We could check if t as is assignable to element type here. And this, you might think this will work, but actually it doesn't, because what we're trying to do is we're trying to check basically if t as here, if this slot is being inferred exactly as element type.
03:55 And even though this looks like that check, it isn't. It's actually checking if t as is assignable to element type. So actually what we need to do is reverse these. When we reverse these, what we're saying is element type, we're checking if element type can be passed to,
04:13 like the specific type of element type can be passed to t as, because if t as is more narrow than element type, then it's actually going to fail. And this means now that if we have exactly element type in this slot there, then we will pass it a instead.
04:33 Whoa, and this actually works. This works, and we still get autocomplete on the as here. So now we're getting full autocomplete. It's behaving properly when you don't pass it an as, so link here is actually being inferred. It's still being inferred as element type. That's the crazy thing, but actually inside here,
04:52 inside the bit that actually matters, we're saying, can you pass element type to t as? Yes, you can. Fantastic. We got a instead, and it's using that as the default. So this is the solution that I found, and I think this works the best. It seems to run pretty quickly as well, which is quite appealing.
05:12 And it means that this link will behave as we expect, even if we're using defaults. And of course, you can still pass custom things to this as well. This behavior hasn't broken at all. So looking good.