Strongly Typing Children in React Doesn't Work
To recap the problem, we are using this branded type to make sure that only Option
elements can be passed as children to the Select
component:
type OptionType = { __brand: "OPTION_TYPE";} & ReactNode;const Option = () => { return (<option></option>) as OptionType;};const S
Transcript
00:00 Okay, so why is this not working? Let's walk through each step piece by piece. Try to understand the type of option type, what's the brand property for? Well, as I discussed in the problem setup, this is really to make this unique here. It means that if we have something that is an option type or something that expects an option type,
00:19 this is the only thing that we can pass here. Branded types are a super, super useful part of TypeScript that we won't be covering too deeply here, but you can check out a resource that I'll link below on branded types that really gives you a good insight as to what they're for. But we're trying to use it here, but it's not working. Why isn't it working?
00:39 Well, let's check out number two. There's an error happening option below. Why is that? Well, it seems that the thing that's here, if we actually just grab that out and put it in a const, const option equals this, this, if we just put that there, is actually not inferred as option type. It's inferred as JSX.element.
00:59 And so select is expecting an option type, but in fact, what it's getting is JSX.element. If we pull this out and if we actually do option here, if we actually call the function, right, because it's just this, this is actually inferred as option type. Isn't that strange? What's the difference here? Well, this, we're declaring this as a JSX.element,
01:19 whereas this actually infers from the return type of the function. So what we're getting here is like this now will work. If we just manually call this option, call it like that, and we get our select here. Isn't that crazy? So this is kind of annoying
01:38 because what we really want is we want this sort of inference, but actually calling these JSX.elements manually is a really bad idea because it takes them out of React's reconciliation algorithm. It means that you're calling them eagerly instead of scheduling them for React to call. So this is a bad, bad, bad idea.
01:56 So what can we do? We can't do anything, basically. Strongly typing children in React is not possible right now. And the reason is, is because it's inferred as JSX.element. This means we can't get any more information about the thing that we're rendering when we render it out on the type level.
02:15 So it means we're sort of stuck, really. We just can't do strongly typing children unless they make some sort of change inside of React or inside of TypeScript to make this possible. I don't mind this. I think this is fine. I think really you can do a lot of this with composition and throwing runtime errors anyway.
02:34 And honestly, if your components really expect a certain type of children, it sort of ruins React's compositional model anyway. You should be able to pass anything to anything and have it all reconcile and work out. But it does make things a little bit annoying and the dream of strongly typed context all the way through the tree,
02:54 I think is just some way off and ends up not being terribly useful anyway. So this was an unfortunate look into an impossible part of React and TypeScript that maybe they might make possible one day. We'll see.