Improving Type Safety with Discriminated Unions in TypeScript
When working with types in TypeScript, discriminated unions play a crucial role in maintaining type safety, particularly when a function is meant to handle a limited set of shapes.
To illustrate this concept, let's examine how the calculateArea
function is used to calculate the area of either a
Transcript
00:00 If we think about this shape type, you can see that really we're only meant to call the calculateArea function with two different types of shapes, either a circle or a square. You can see that that's borne out by the way that we're calling it down the bottom. We've either got kindSquare or kindCircle.
00:18 So we can technically kind of like sharpen this up by using literal types too, right? We can say kindCircle or square.
00:28 And we're doing okay here but we've actually not gotten that far really because you can still pass a square with a radius, you can still pass a circle with a side length.
00:40 That's not great. What we need to do is we kind of need to split these out into two separate kind of objects and that will give us the things that we need. So if you think about it, let's actually just define a type square and type square is going to have a kind of square.
00:58 And it's also going to have a side length property. So let's just move this down to here. Now this square, if you just think about this type yourself, that square now has a side length which might be defined.
01:11 No, that's not right. Side length, if we have a square, it's always going to be defined. You can't have a square without a side length, even if it's zero. It's never going to be undefined. And this shape now, that's no longer just a shape, it's actually a circle. And the radius, again, you can't have a circle without radius. So let's make that defined too.
01:30 So now our shape type down the bottom here, this now no longer has a type. So let's actually just say a shape is a union. It could either be a circle or a square. And now we've found our solution. You notice now all of the undefines have disappeared.
01:47 We're now certain that if the kind is a circle, it's going to have a radius. Otherwise, it's going to be kind, square, side length, number. So let's take a look at our function. Our function now is saying calculate area, shape, shape. And if we say shape.kind equals circle, you can see that kind is either going to be circle or square.
02:06 Then what we get is shape.radius is now defined. And if we hover over the shape inside our function that calculates the area, you can see that it's been narrowed down to circle. What? And then inside the else scope just here, we have shape.sideLength.
02:24 And we can see that inside there, it's square. Amazing. If we were to move shape.sideLength out, in fact, let's just sort of remove a couple of these return statements,
02:36 you can see that only inside the proper scope where it's narrowed is our narrowing actually being applied. And just by checking the kind, we can also check the other parts of the union. This is what's called a discriminated union.
02:53 And you'll notice that a discriminated union always has basically a discriminator, the thing that's in common between all of the types that are part of it. So in this case, we have two object types. We have square, we have circle, and the shape is the discriminated union.
03:08 If we removed kind and we made one of them type like this, then the whole thing breaks down. There's now actually no properties in common between circle and square. And so if you were to just check shape like this, we're not getting any autocompletes because there's no properties in common,
03:25 meaning that any property we access is not going to like us. It's not going to be happy. So if we change back our type to kind, now there's one property in common, which is kind.
03:37 And now we're going to get kind on there, meaning that we can check it in an if statement to check which branch of the union we're on. And of course, discriminated unions can have other properties in common as well, but there's always one discriminator. That's usually the one that's used to check if you're on one branch or the other.
03:55 So this is a discriminated union. It's so useful in so many different situations. It's quite hard to get your head around. And the fact that it takes in literals and like narrowing and also takes in union types as well,
04:09 it means it's a really powerful, pretty advanced way to use TypeScript and build out the sort of shapes of types and the APIs that you want.