Don't use return types, unless...

Your default attitude towards return types should be 'don't use them', unless one of three things are true:

  • You're writing a function with multiple branches
  • You're building a library
  • You have a specific TS perf concern and you're banging your head against the wall


[0:00] What's up, wizards? We are going to be looking at return types today. Return types in TypeScript give you the ability to specify what you think a function should return. Here, we have a function called makeId. This returns a string of ID with a random number of length 16 appended to the end of it.

[0:16] If we want to, we can guarantee that this function returns a string by adding string here. Now we can see that result is result string. We could force makeId to return a number instead by changing this to number. Now we get an error here saying, "Type string is not assignable to type number."

[0:30] The result down here changes its type to match the return type. This means you can use a return type to force a function to return a certain thing. If we remove this number, you can see that result still gets inferred properly. That's because TypeScript is smart enough to understand that this returns a string.

[0:47] The question is, should we add the return type or should we not? Most of the time, you should not be using return types. In simple functions like makeId, TypeScript knows what you're returning. You don't need to tell it something that it already knows. This return type ends up just being extra code that you need to maintain.

[1:04] The rule of thumb should be not to use return types and let TypeScript infer the result on its own. This changes, though, when you have multiple branches inside your function. What do I mean by a branch? Let's imagine we have a function called handleNewState where we're taking in two different types of events. We have a window focused event and a window blurred event.

[1:22] This handleNewState function is going to return a new state based on the event that's been passed. In window blurred, we return one state. In window focused, we return another state. Let's take a look at the new state that we get when we handle the window blurred event.

[1:35] This new state is pretty messy. We've got what looks like latestUpdate with an uppercase U and latestupdate with a lowercase u defined here. This looks really, really fishy to me. The only things that we intended to return were isFocused Boolean and latestUpdate number. Let's create a new type that describes that.

[1:52] Now we'll mark handleNewState return type as state. You can see that in one of the branches here, latestupdate is with a lowercase. We can fix that by changing it to uppercase. Because of this return type, we're now sure that handleNewState will return what we want it to.

[2:04] If we add a new event called window closed, then we can be sure that this function still returns what we want it to, making this code easier to extend. If you have more than one branch in your function, then you should be using a return type. That means an if statement. That means a switch statement. That probably also means a ternary as well.

[2:22] By default, don't require your devs to mark every function with a return type. There's an ESLint rule for that, and it's my least favorite one. I hate it. Some devs in some projects have it turned on. There is an exception to this, though. If you're writing library code, you should always declare return types.

[2:38] Let's say that we wanted to bundle our handleNewState function into a library. We should add an export to every type that that function consumes. Let's say we're inside an application that wants to consume from this library. We can create a function called wrapHandleNewState, which just takes handleNewState and passes an event to it.

[2:54] The issue is, how do we type this event? We can just use the type that we get from the library. If we want to handle our own logic inside here, then we probably also want to add a state return type here, too. This means that consumers of your library can use the types that you've used to type your functions.

[3:10] There's also some stuff in the TypeScript Performance Wiki about libraries using return types. If, as a library, you use return types, you're going to see a faster bundle speed, because these types are easier to emit from TypeScript than anonymous ones.

[3:22] For the folks saying, "I use TypeScript return types because of performance," that's really only for when you're printing declaration files in a library setting, which is a legit use case, but it doesn't really map up to application code.

[3:34] I have been proved wrong on that point before. I've had people bring to me TypeScript performance problems where just a return type was the solution for some magical reason. If you're getting some really, really slow TypeScript compilation in your application, you may want to consider adding return types if the return type is really, really complicated.

[3:53] To sum up, in application code, don't by default use return types. Use them if your function has multiple branches. Use them if you're writing a library, definitely. Use them if you have any weird niche performance concerns to do with really big objects being passed and inferred through your code. You can turn that ESLint rule off that forces you to have return types, please.

When should you use return types? The use cases are narrower than you might think...

Discuss on Twitter

More Tips

Assign local variables to default generic slots to dry up your code and improve performance