Home > OS >  Why specify function return types?
Why specify function return types?

Time:11-18

Most of the time I read that we should use type inference as much as possible. When writing a function I understand we must type arguments since they cannot be inferred, but why do we have to type the return value? TypeScript is taking care of that. What are the benefits of explicitly typing the return value of a function? So far I only read that I should do it, but no one is saying why.

CodePudding user response:

The compiler can infer what your code does, but it has no idea what you intended. Take this trivial example:

function thing(value: string) {
    return value === "foo" ? 123 : "456";
}

The inferred type is function thing(value: string): 123 | "456", and that matches the implementation, but is that in any meaningful sense right? Perhaps I intended to always return a number, for example; if I tell the compiler that, it can tell me I didn't:

Type 'string | number' is not assignable to type 'number'.
  Type 'string' is not assignable to type 'number'.(2322)

Particularly when you're using complex wrapper/generic types (e.g. I see issues around this a lot in where RxJS observables are being used), this can really help getting early feedback on your assumptions.


I also work a lot with test-driven development (TDD), where one of the values of writing tests before implementation is that it gives you the opportunity to discuss the interface at a time where the cost of changing it is close to zero. Using the classic RPS example:

function rps(left: string, right: string) {
  return "right";
}

it("returns 'right' for 'rock' vs. 'paper'", () => {
  expect(rps("rock", "paper")).to.equal("right");
});

That'll compile and pass, but is it what we want? Now we can talk about options:

  • Do we accept the inferred type function rps(left: string, right: string): string?

  • Go more specific with e.g.

    type Throw = "rock" | "paper" | "scissors";
    type Outcome = "left" | "right" : "draw";
    function rps(left: Throw, right: Throw): Outcome { ... }
    
  • Use enums instead?

We can talk through the trade-offs and pick the best option given what we know at the time. The explicit return type serves as documentation of the decision we made.


I'd recommend turning on @typescript-eslint/explicit-function-return-type if you're linting your code, which provides as its rationale:

Explicit types for function return values makes it clear to any calling code what type is returned. This ensures that the return value is assigned to a variable of the correct type; or in the case where there is no return value, that the calling code doesn't try to use the undefined value when it shouldn't.

CodePudding user response:

The main reason to annotate the return type of a function is the same as the main reason to use Typescript at all: because you want the compiler to check your code and give you useful error messages when you make certain kinds of mistakes.

If you let Typescript infer the return type of your function, then whatever it infers will be the return type of the function, even if the inferred type is not the type you intended. In this case, if you try to return something of the wrong type, the compiler will use that wrong type as the function's return type and you will get an error when you try to call the function and expect something of the right type - or worse, you won't get an error, but your code will fail at runtime or do the wrong thing.

On the other hand, if you know what you want the return type to be, and you want the compiler to check that your function actually does return something of that type, then you should use a type annotation. In this case, if you try to return something of the wrong type, then you'll get an error in the function itself, where the mistake actually was.

CodePudding user response:

Here is one good reason to use explicit return types. According to TS wiki:

Adding type annotations, especially return types, can save the compiler a lot of work. In part, this is because named types tend to be more compact than anonymous types (which the compiler might infer), which reduces the amount of time spent reading and writing declaration files (e.g. for incremental builds). Type inference is very convenient, so there's no need to do this universally - however, it can be a useful thing to try if you've identified a slow section of your code.

Hence, if you don't have any problems with compliation performance I think it is not required to specify return types

Here you can find another question/answer regarding compilation performance. It also relates to TypeScript Performance wiki

P.S. you can use explicit return type for disallowing using some extra properties of return value. Consider this exmaple:

const foo = (): { age: number } => {
    const result = {
        age: 42,
        name: 'John'
    }
    return result
}

const result = foo()
result.age // ok
result.name //error

As you might have noticed, explicit {age:number} return type is a super type of a return value type. But it is weak, because it does not work if you want to return literal object, like here:

const foo = (): { age: number } => ({
    age: 42,
    name: 'John' // error
})

So I can't recommend using this technique, however it worth knowing.

  • Related