Stop Writing TypeScript Code Like This
Chapters6
This chapter highlights how TypeScript sometimes infers return types automatically, but there are many scenarios where explicitly specifying a return type improves code quality. It also mentions a TypeScript utility types cheat sheet as a helpful resource.
Specifying return types in TypeScript is crucial for recursion, library stability, and precise data shapes—use strategically, not for tiny helpers.
Summary
Web Dev Simplified’s video by Joshua shows why return types matter in TypeScript and when to embrace them beyond automatic inference. He kicks off with the single scenario requiring explicit returns: recursive functions, using a Fibonacci example to show how TS can’t infer the return type from recursion alone. He then contrasts this with the common pitfall of unintended undefineds sneaking into a function’s signature. The discussion moves to a practical example: a saveUser service that may return a User, null, or undefined depending on validation, and how exporting a User type helps catch signature drift. Joshua argues that return types shine in library-style code or shared utilities used across a codebase, where changing a signature could break callers. He also touches on when not to use return types—small, one-off helpers and local constants where the overhead isn’t worth it. The video also covers how explicit returns can aid AI code generation and how to model precise data shapes with tuples and discriminated unions. He promotes a TypeScript utility types cheat sheet for deeper mastery, linked in the description.
Key Takeaways
- For recursive functions like Fibonacci, always specify the return type because TS cannot infer it across self-calls and will default to any.
- When a function may return null, undefined, or a value (e.g., saveUser can return User, null, or undefined), export and import the defined type to enforce a strict return signature across the codebase.
- Return types are especially valuable in library-style code or helpers used throughout an app to prevent accidental signature drift during refactors.
- Avoid return types for small, single-file helpers or inline arrow functions that are local to one file, where the risk of breaking elsewhere is low.
- Using a hard-coded return type (e.g., a tuple like [number, string] or a discriminated union) clarifies intent and narrows possible outputs beyond what inference provides.
- Explicit return types can aid AI-assisted coding by removing the need for the AI to infer what a function returns from its implementation.
- When modeling complex returns (e.g., two different object shapes or a union with common fields), a well-chosen return type helps TypeScript narrow the data safely during usage.
Who Is This For?
Frontend and TypeScript developers who write large apps or libraries and want to lock function contracts to prevent breakages during refactors. Also useful for teams adopting consistent return-type practices across shared utilities.
Notable Quotes
""anytime a function calls itself, TypeScript doesn't know what the inferred return type is because obviously it doesn't know what the return type of Fibonacci is.""
—Explains why recursion requires an explicit return type.
""the issue is though is that now we are returning either a user or null or here we're returning undefined because that's the default if you don't actually return a value.""
—Demonstrates how changing logic can alter the inferred return type and cause downstream errors.
""Having return types can help you when you're modifying a function to make sure it still sticks to the exact same signature that you expect that function to have.""
—Highlights why explicit returns protect downstream code during refactors.
""if you're creating a library to use by other people, obviously, use return types for your functions that are exposed in that library.""
—Advocates for strict signatures in public APIs or shared utilities.
""If you're using AI to write a lot of your code, having those return types can actually help the AI understand what different parts of your code does.""
—Notes a practical benefit of return types for AI-assisted coding.
Questions This Video Answers
- When should I add explicit return types in TypeScript for maximum stability?
- What is the difference between returning null, undefined, or a value in TypeScript functions?
- How do I model strict function outputs using tuples or discriminated unions in TypeScript?
- Should I always type return values for library code or only for public APIs?
- How can TypeScript return types help with AI-assisted coding tools?
TypeScriptReturn TypesType InferenceRecursionDiscriminated UnionsTuplesas constLibrary DesignCode QualityAI-assisted Coding
Full Transcript
One of the best features of Typescript is their inference and specifically when it comes to return types where they're just automatically inferred for you without you having to manually specify them. This is great because manually specifying return types could be quite long and convoluted. But there are also many scenarios where you need to specify a return type to have good highquality code. In this video, I want to talk about all the scenarios where specifying a return type will give you better code so that way you can make sure you're writing the best possible TypeScript code.
Also, if you're interested in really taking your TypeScript skills to the next level, I recommend checking out my TypeScript utility types cheat sheet. It'll be linked down in the description for you. It's completely free in light and dark mode and can really help take your TypeScript code to the next level. Welcome back to WebDev Simplified. My job is to simplify the web for you so you can start building your dream project sooner. And before we dive into some of the more opinionated nuance, I do want to talk about one scenario where you are required to use return types in Typescript.
And that is anytime you have a recursive function. For example, I have this Fibonacci function which takes in a number and it's going to calculate the Fibonacci sequence for that number specifically and it calls itself because that's the recursive version of this function. And anytime a function calls itself, TypeScript doesn't know what the inferred return type is because obviously it doesn't know what the return type of Fibonacci is. So anytime you have a recursive function, you need to make sure you specifically type the return type. In our case, this is a number. And that'll actually show us that we had errors in our code down here.
because if you don't type the return type of a function, it defaults to a return type of any which is obviously not very useful. So I just wanted to get this out of the way quickly because this is the one time where a return type is required. Now next I want to talk about some of the advantages or disadvantages of using a return type. Let's take this very simple example. You can see here I have this save user service which we can call and validate some user input like making sure the age is a proper number and so on.
And we call out to this save user database. You can just imagine that this is calling out to like Drizzle or some other database client. In our example, all it's doing is it's just either returning null if it's unable to save the user or it returns us the user object. A relatively simple function. You can see here we have that save user function. It returns user or null. Now in our case, we want to add our validation to this save user service. So to do this is rather straightforward. We can just say if our age is less than zero, we have some type of problem.
So if we have some type of problem, maybe we want to log out some error that we have some issue with our code. And otherwise, we may just want to return that we don't have a data value here. So we'll just return undefined, saying that we don't have a particular user for this. It seems like everything's perfectly fine because, you know, that's kind of how our save user DB works. If we have some type of error or something goes wrong, we just return no data. In this case, we return null. The issue is though is that now we are returning either a user or null or here we're returning undefined because that's the default if you don't actually return a value.
So our function now returns a user null or undefined. One of those three values while before we added this code it only actually returned null or a user. And by adding this little bit of code other places where we used the save user service are now broken because it only expected us to return null. And now you can see down here, this new user could be user or undefined because we're not actually checking for it. This is a bit of a contrived example, but you can see how easy it is to make a mistake like this where you modify your code and the inferred return type from TypeScript changes cuz your code changes, but it actually impacts the rest of your code in a non-trivial way.
This is an instance where having a return type may be useful. For example, if you know the return type of this needs to either be user or null, you could specifically say that. And if we just make sure we export this user type up here and we import that user type here, we're now going to get an error inside the function saying, "Hey, we can't return undefined because it's not of the type user or null and we need to make sure we return null instead here, which now fixes the errors in the rest of our code." So having return types can help you when you're modifying a function to make sure it still sticks to the exact same signature that you expect that function to have.
Obviously, the downside of doing a return type like this though is that you essentially are duplicating a little bit of code. You're specifying what the return type looks like here and then you're specifying often a very similar shape inside of your function. So when is it time to use a return type and when is it time not to. In my opinion, by default, I would recommend not using return types for any of your functions. Just by default, leave off return types. And for the most part, that's going to work fine for many projects. As your project starts to get larger though, there are specific scenarios where a return type makes sense.
And one of those scenarios is if you're writing a library. Obviously, if you're writing a library that other people are going to use, having these hard-coded signatures that say exactly what something is going to return makes it so that as you update that library, you don't accidentally change the return type of a function, which may break other people's code, even if it doesn't break the code that you're currently working on. So, if you're creating a library to use by other people, obviously, use return types for your functions that are exposed in that library. But even if you're not creating your own library, let's just say you're creating a helper function inside of a large project.
For example, this save user service function is a function that's going to be used many places in our application everywhere we're saving a user. So, this is something that is almost akin to library code. It's not technically a library, but in our particular application, it works as a library in our code. That's a scenario where it may make sense to add on a return type because our code is kind of acting in the capacity of a library. And when we update this code, we want to make sure that it still conforms to this return type and that we don't accidentally make a mistake by returning undefined instead of null or some other thing in that similar vein.
And the main reason why I do recommend a return type for this style of code is because this thing is going to be used throughout your entire application in many different places. So you're not always able to see the exact impacts that changing the code in this function may make on the rest of your codebase. While if for example we had a simple function, let's just come down here and we're going to create a simple function. It's just going to be a random helper function that is only useful for this file. We're not even exporting or anything like that.
This type of function where it's just a small helper that you're using in one-off type of scenarios. This does not make any sense to write a return type for because if you mess up the type inside this function, accidentally return the wrong thing, it's only going to affect the local file you're inside or maybe a very small subset of files, which is easy for you to see because it's essentially a locally scoped function which you're really only using in a few places. It's pretty much the exact same thing with helper functions that are arrow functions as well.
Let's just say we had like a add event listener on here and we wanted to write out a function like this that's going to return something if this function actually did return something. It doesn't make sense to add return types for this type of function or any other type of small one-off function, especially if you're writing like a constant variable like this that's just going to be a simple function. Again, it doesn't make sense to write return types for this because it's a very simple and straightforward function. Now, another scenario that a lot of people don't really talk about is a great use case for a return type is actually if you're using AI inside your code.
If you're using AI to write a lot of your code, having those return types can actually help the AI understand what different parts of your code does. So having more return types in your code can make it easier for your AI to parse the code instead of having to also infer the types just like TypeScript. It can essentially look at the code and say, "Oh, I know what that type is based on what the actual function definition is cuz it's set there right in the return type." Now, another instance where you can or cannot use return types.
And I think return types are really useful is when you're trying to return very specific values. Let's just say that we have a simple function here. It doesn't really matter what this function does. And inside this function, we're trying to return an array where the first value is a number and let's say the second value is a string. When I hover over this function, you're going to notice that it returns to me a string or a number array. So, it doesn't actually know that the first value is always a number and that the second value is always a string.
It just assumes it's an array with strings and numbers in there mixed about. One thing you could do to fix this is you could put an as const after this. Now, if I hover over this, you can see that it's a readonly array that always the first value is the number one and the second value is the string k. So at least we now know the first value is a number and the second value is a string. But by adding as const we have marked this as read only which may not be what you want.
And since we have hard-coded values it's using the values of one and a string directly. Another way to get around this is instead to use a return type. Here I can say that I'm going to return an array or a tupil in this particular case that starts with a number and the second value is a string. Now when I hover over this you can see I get an array where the first value is a number and the second value is a string. And if I were to call this function and try to get the data out of that, you can see if I dstructure this, the very first value I get out of here, if I hover over it, is a number, while the second value is a string.
And again, if I don't have that hard-coded return type, the first value could be a string or a number, and the second value could be a string or a number. So, anytime you want to kind of restrict what the return type of something is, adding that specific return type is really helpful. This also works even with like a string. Let's say I want to return the string left, for example. From here, we'll get rid of the hard-coded return type. If I hover over this T, you can see it just returns a type of string. It doesn't know if it's the string left, right, and so on.
But if I know I always want this to return either the string left or the string right, I can just come in here and I can say that it's either going to be the value left or it's going to be the value right. This is another use case where having hard-coded return types is really useful. Or you can do the exact same thing by using as const here. And now you can see the only return for this is left because that's the only value I'm returning. Another very useful case for specific return types is when you want to return two different sets of data based on whatever you pass in.
Let's just say we pass in a boolean here. And if the boolean is true, I want to return one thing. So if B, then I want to come in here and we'll just return an object that says error true with a message that's just whatever my error message is. But if I don't have an error, I don't have that boolean set to true, but it's set to false. Instead, I want to return something entirely separate. It doesn't matter what it is. Let's just say I return a string. So when I look at the return type of this function, you can see that it is this string or it's going to be that error boolean type right here.
Now this works because our object and our string are two different types. But what happens if we return things that are quite similar here? I'm going to return something that instead of having an error and a message is going to have data, which we'll just set to whatever. It doesn't really matter. We'll make it a string. And we'll just put some other property on here as well. And then finally, we'll put a message on this one as well. So now we have a bunch of different properties being defined on this object. And we hover over this type T.
You can see we kind of an interesting object. You can see error and boolean are required here. Data and other are kind of this undefined thing that is not required. Same thing here. We have some weird different types going on inside this thing because it doesn't quite know how to narrow this down. This gets even more complicated when you have bigger objects or more complex if conditions. So, this is a type of scenario where it may make sense to actually add a hard-coded return type where in one case you're going to have an error which is going to be true in our case because it's always true.
And you're going to have a message which is going to be a string. And then we can specify our second return type which is going to be very similar to the return type up here. We have data which is a string in our particular case. We always have a message of string and then finally we have our other value which is also a string. Now when I hover over this type T, you can see we don't get those weird additional extra undefined types being added on because they don't actually make sense in our particular scenario.
And if we try to use this by just getting a variable back by calling D, we'll just pass it in true here. If I try to look at my value of A, you can see message is the only thing being defined on this particular object because it's essentially one of these two different types and I need to narrow it down further from there if I needed to. And again, the reason message is the only thing that shows up is because it's the only thing that's shared between these two. Usually when you do these types of returns, you may have a specific key that's different.
So in our case, this one may say error of false. And then we could specify an error of false here. And now down here, I can essentially do a check where if my error is true, then I get one particular type of object. So you can see I get error and message. And if it's not true, you can see I get different data being returned to me. So it's an easy way for me to narrow that down. And this is usually done best when I have a hard-coded return type. If I actually remove this hard-coded return type, I get interesting stuff.
You can see here it doesn't matter if my error is true or not. I still get the exact same data being returned where I have this weird string or undefined for this one. Same thing with our other here. This is a string or undefined. So without those hard-coded return types, I get really interesting data being returned which is harder for me to use. Now this is just one of many different things to learn about TypeScript. If you want to go deep into how the different TypeScript utility types work, I have a full cheat sheet. It's in light and dark mode that you can download.
I'll link it in the description for you. It covers every single custom type that's a utility type in TypeScript that you need to know to really write senior level TypeScript code.
More from Web Dev Simplified
Get daily recaps from
Web Dev Simplified
AI-powered summaries delivered to your inbox. Save hours every week while staying fully informed.









