Ensure Runtime Level & Type Level Safety with Conditional Types
Let's build this solution together from our starting point:
Adding a Type Argument
0:00 Let's build this one up from scratch and see where we get to. We know that this greeting here, which is currently typed as unknown, we're going to have to capture that in a type argument. We need to know whether it's hello or goodbye that they're passing in so that we can pass the opposite back.
0:16 What we're going to do is we can put in TGreeting here. We'll just capture it in TGreeting. Whenever you do that and you have some function call examples, you should check whether it's inferring properly. Actually, it's not. It's being inferred as string here.
0:32 We can help this along if we want to. We can say, "extends string." Really, we know that this is either going to be hello or goodbye here. What this does, it means you can't pass in something random like blah, blah, blah, blah, blah, which isn't part of the test but is probably something we should include here.
0:50 We've got our greeting TGreeting that's being inferred properly, but we're still getting the union type back. What we should do is type the return type. We're going to do this with a conditional type. TGreeting, we're going to check if it extends goodbye. If it does, then we'll return hello. Otherwise, we'll return goodbye.
1:13 Now, all of our tests are passing, actually. We have this result being typed as goodbye when we pass in hello. We've got hello being passed in there. Because of our constraint, we even get autocomplete here too, which is really nice. Now, if I pass in goodbye, I'm going to get hello. It's very, very pleasing, lovely, but there's an issue.
1:32 Blah, blah, blah, blah, blah is not assignable to type TGreeting extends good...There's a long type error here. There's a real issue here. A lot of people run into this when they first start doing conditional types in return types, which is that really this function...
1:53 TypeScript isn't smart enough to match this runtime stuff, all of these little nodes here, the flow of this code, onto your type. It can't tell that those things match. You need to do an as inside here.
2:09 The cleanest way to do it would be to wrap these in parentheses and say, "as any" here. This may look completely bonkers to you, but it's one of the only ways to actually tell TypeScript that this is what you were trying to do, that you know better than TypeScript in this situation.
2:25 If we look at the second solution here, then this just takes it and wraps it in a type helper there. What we get is you can actually just as that whole thing as the GreetingResult TGreeting. This actually replaces the return type
2:44 If we look at the other one again, actually pull this on the other side here, then what we can see is -- I'll zoom out just a touch -- on this side, we're doing the typing in the return type. On this side, we're doing the typing in the as. I can actually just swap this out if I want to. It still will work there. You get result being goodbye, result being blah, blah, blah.
3:09 The idea is that this raw statement here, TypeScript isn't clever enough to actually map a conditional type on it. If you're using a conditional type in the return type of a function, you're probably going to need to use as somewhere.