I never understood statements like this one. To be honest, even if you declare the return type of a function, you can and will forget it after you've written many lines of code, and you will still have to return to the line in which it's declared using the search function of your text editor to check it.
It's not about you forgetting the return type -- this is always going to happen. It's about the tool being able to let you know that you forgot the return type.
As an addition, as functions are declared with type
funcname()..., whitout knowing type you will have to search over each line in which the function is called, because you only know
funcname, while in Python and the like you could just search for
def funcname or
function funcname which only happens once, at the declaration.
This is a matter of syntax, which is completely unrelated from static typing.
The C family syntax is indeed unfriendly when you want to look up a declaration without having specialized tools at your disposal. Other languages don't have this problem. See Rust's declaration syntax:
fn funcname(a: i32) -> i32
More over, with REPLs it's trivial to test a function for it's return type with different inputs, while with statically typed languages you would need to add some lines of code and recompile everything just to know the type declared.
Any language can be interpreted and any language can have a REPL.
So, other than to know the return type of a function which clearly isn't a strong point of statically typed languages, how is static typing really helpful in bigger projects?
I'll answer in an abstract way.
A program consists of various operations and those operations are laid out the way they are because of some assumptions the developer makes.
Some assumptions are implicit and some are explicit. Some assumptions concern an operation near them, some concern an operation away from them. An assumption is easier to identify when it is expressed explicitly and as close as possible to the places where its truth value matters.
A bug is the manifestation of an assumption that exists in the program but doesn't hold for some cases. To track down a bug, we need to identify the erroneous assumption. To remove the bug, we need to either remove that assumption from the program or change something so that the assumption actually holds.
I'd like to categorize assumptions into two kinds.
The first kind are the assumptions that may or may not hold, depending on the inputs of the program. To identify an erroneous assumption of this kind, we need to search in the space of all possible inputs of the program. Using educated guesses and rational thinking, we can narrow down the problem and search in a much smaller space. But still, as a program grows even a little bit, its initial input space grows at an enormous rate -- to the point where it can be considered infinite for all practical purposes.
The second kind are the assumptions that definitely hold for all inputs, or are definitely erroneous for all inputs. When we identify an assumption of this kind as erroneous, we don't even need to run the program or test any input. When we identify an assumption of this kind as correct, we have one less suspect to care about when we're tracking down a bug (any bug). Therefore, there is value in having as many assumptions as possible belong to this kind.
To put an assumption in the second category (always true or always false, independent of inputs), we need a minimum amount of information to be available at the place where the assumption is made. Across a program's source code, information gets stale pretty quickly (for example, many compilers don't do interprocedural analysis, which makes any call a hard boundary for most information). We need a way to keep the required information fresh (valid and nearby).
One way is to have the source of this information as close as possible to the place where it's going to be consumed, but that can be impractical for most use cases. Another way is to repeat the information frequently, renewing its relevance across the source code.
As you can already guess, static types are exactly that -- beacons of type information scattered across the source code. That information can be used to put most assumptions about type correctness in the second category, meaning that almost any operation can be classified as always correct or always incorrect with respect to type compatibility.
When our types are incorrect, the analysis saves us time by bringing the bug to our attention early rather than late. When our types are correct, the analysis saves us time by ensuring that when a bug occurs, we can immediately rule out type errors.