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 angular 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.