Advanced Patterns 11 exercises
solution

Distributive Omit with the `as` Prop

Here's how I got everything working.

The first thing I did was pulled in the FixedForwardRef from the previous exercise that preserved the generosity of what was passed in.


type FixedForwardRef = <T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactNode,
) => (props:

Loading solution

Transcript

00:00 So, this is a story all about how I tried to get this working. I knew when it's a fresh Prince of Bel-Air there. And I got this working finally by doing a few things. So, the first thing I did was pulled in the fixed forward ref from the previous exercise that we had

00:16 that preserves the generosity of passing components. This is important. I also pulled in this distributive omit type, which I will explain in a minute. And the first way I thought about this is I thought, okay, we need to get this ref type working. The component props without ref doesn't

00:35 seem to be doing the job. And the reason this isn't doing the job is because, like, it's not inferring the ref properly. So, this is typed as react.ref any here, which is no good. And it makes sense that we want to actually, like, pull this in here. So, we need to somehow

00:52 infer the component props with ref in this situation. So, now we've got component props with ref. And this is what it's useful for, right? It's taking in the element type that we've got by using this sort of conditional type to map over the A there. And so, this should work. And it works actually in most cases. So, we're seeing here that ref is now

01:11 typed as, if I pull this open, it should be, yeah, it's got instance HTML anchor element. So, all good. Though, we're still getting some strange errors. And I'm going to ignore this error for now. And I'll show you why it's happening, because it's a crazy error.

01:28 Now, this one here is a ref. And this ref is still not quite working. So, it seems to be when we pass in, let's say, span or something like this or button, then this version is not quite, like, working yet. HTML button element is missing the type from HTML anchor

01:45 element. So, it seems to be that this ref is still thinking that it's an anchor element, not a button element, at least in the ref. So, the way that I got this working, because I'm still not sure why that particular error is happening, but what I figured was happening

02:02 is that it wasn't picking up on the as properly. So, if we just have a look at link actually and pull this down here, then if we don't pass in anything, then you should see that it's inferred as element type as before, right? So, for God's sake, my thing's not working.

02:18 Okay, my hover doesn't seem to be working. But if I say as button like this, then if we take another look here, then it's still being inferred as element type. So, it hasn't actually picked up from the button. So, something's happening now that we're using component props

02:33 with ref to sort of obscure the inference of the as, which is troubling. So, what I did to get that working was I wrapped this in an omit so that this section here, this whole tamale, this thing there is basically just being wrapped in like don't ever have

02:52 as in here, because I wanted to say, okay, don't, I want to make sure there's no conflict between this and this. And basically, this is the only source of truth. And now that actually ended up working. So, if we look at link now, link, if we take a look at as

03:13 will basically now follow what's in here. And if we hover over it, it's now being inferred as button. Yes, first victory. But what happened then was I got this error continues to happen. This is a really, really, really strange error. And I'm going to go into detail on it. This

03:31 on click here is a react dot mouse event handler HTML anchor element or undefined. And like, if you hover over this section, it's working great. But if you hover over this section, it says it implicitly has an any type. And it seems to me that TypeScript is the same

03:49 things happening on this button as well, by the way. So, we've got an on click HTML button element. Something is happening here, which I do not understand, or rather, I think it might be a small bug in TypeScript that we uncovered during this. And when you have situations

04:05 like this, where, because if you look at this, TypeScript is trying to make sense of this props type here. And it's looking at, let's say, this as and this omit. And it's seeing that, okay, element type could be a bunch of different things depending on what

04:21 they pass in. And it's trying to resolve all of those different props in there. And I think our omit is breaking something. You should know that omit, if we look at type, let's say we've got a union, let's say it's got an A property on it, which has an A, or it's

04:41 got a B property on it, which has a B. Now, if we take a look at this union, and we might think that, okay, we can, let's say we have, I'm not sure, like a user string, for instance,

04:53 and then a user string. If we say type without user, and we try to use omit union user, you might think that this is just going to return us like the same structure, but just without the user, right? But if we hover over it, we can see that it actually returns an empty

05:11 object. That's because omit is not distributive. And distributive basically means that it doesn't distribute over each member of this union. Whereas if we were to use something like a

05:26 distributive omit, it would actually go through each branch here and return the proper value. So if we say distributive omit, just there, so on the without user, we've now you can see that it's calling omit on each level here. So omit this and omit this. And I will link

05:44 below why this works and basically link to the section of the TypeScript docs on distributive types, because what's happening here is we're basically when you use a conditional type, it will make it distribute over the entire union that's passed in. So we're passing in

06:03 T here. And we're also passing in T omitted, which is the thing that we want to omit from the thing. So now what it's doing is it's saying, okay, T extends any, forcing it into a conditional making it distribute over the whole thing. And then we call omit on that

06:19 thing. It's a confusing property of TypeScript. And I tried it basically as a Hail Mary here. And it worked. So distributive omits called on this, it seems to somehow resolve all of the issues that we've been having. And when it catches up, it now works. And now E is

06:39 inferred properly as HTML anchor element. So to go back through, we're using the same default trick as we had before. We're now using component props with ref instead of component props without ref, we need to some for some reason, remove the as here in order to kind

06:57 of make sure that this is the only inference sites for where T as can be picked up. And we need to use a distributive omit, not a normal omit to get it working. God, it was such a relief to get this working, I have to say. But this is now something that you

07:16 can copy and paste into your projects that you know, will work in all situations for generic components for, for the as prop, it will work with forwarded refs, the refs will be properly inferred. And this I think is forwards compatible and possibly backwards compatible too. I don't see anything here that wouldn't work on previous TS versions.

07:36 So there we go.