Note how some of these are directly at odds with writing easily maintainable code. For example, using type annotations for return types [0]:
- import { otherFunc } from "other";
+ import { otherFunc, otherType } from "other";
- export function func() {
+ export function func(): otherType {
return otherFunc();
}
Not manually annotating the return type here reduces both the work you need to do when refactoring and the visual overload when working with the code. In my opinion, both of those are far more important than small changes in compile time.
I honestly find it so annoying when return types aren't annotated. It's considerably less overhead for me when I can look at the signature and see the return type, even if a few characters are added.
Agreed. I think Rust had the right approach here: require type annotations for function parameters & return values, but perform type inference within functions. That makes it obvious when a function's public interface (so to speak) has changed.
Not really, rust is unusable for interactive programming because of this reason. I understand that's not rust target domain but still there's downsides to be had with their approach.
You mean like a REPL? Most languages are unusable for interactive programming. And it's not a downside if it stops use in a domain that was nobody's goal.
Yes, just pointing out the trade offs here. Btw many functional languages have quite good interactive experiences, (ocaml, f sharp, Haskell , clojure all have decent repl's). Usually they work instantly for small sized projects.
Have you ever seen a procedural language with a good REPL? Python maybe, but definitely no compiled procedural garbage-collectorless language. The "needing to put type signatures" is completely unrelated. I have no idea how that would stop Rust from being good in a REPL.
Can’t speak to its quality, but there’s nothing stopping someone from writing a repl from a sufficient compiler API... and “good” is only limited by inference quality and runtime performance. IDEs are pretty snappy at showing you autocomplete as you type regardless of whether the language has a garbage collector. And performance - well, that’s what caching would be for, and an optimized compiler design that only needs to recompile changed code...
I would also point out the “auto” keyword in CPP likely saves folks a lot of typing ;-) I know it and similar inferences changed my mind on the whole static vs dynamic debate...
Exactly, and if I declare that a function returns a string, and then edit its code and add a return statement that returns a number by mistake, the compiler will tell me right away.
If I didn’t declare the return type then the function will silently now infer the return type to be “string | number” (which might or might not break compilation elsewhere).
My experience with f sharp and recently with the new Haskell langauge server has been that not having written type annotations is a no issue because the language tooling still shows them above the function definition and better yet can autogenerate them.
So I think this is more of a tooling issue, do keep in mind global type inference is really handy for interactive programming in the repl and short scripts.
I've not had the chance yet to play with OCaml or Haskell in earnest, but it's on my list.
One thing that I'm fascinated and terrified by is the global type inference.
Doesn't it get really hard to figure how how you're allowed to call things?
Does it make your IDE experience slow? Similar to one of the things mentioned in the OP, I would think that the type hints would be super helpful to the compiler/analyzer.
I would say it works surprisingly well 90% of the time.
The main help type hints give to the compiler is that it can generate better error messages for when things do go wrong, apart from that there is no major difference.
As I mentioned earlier it's a powerful feature, and has its uses, especially when you are still trying to figure the types of your program.
For an easier taste of global type inference you can try elm language, it's also a good stepping stone to learning haskell
Looking at the votes on my comment jump up and down, I was very surprised to find that my opinion is apparently very controversial. I find this very intriguing, and I'd like to hear from your side. From your perspective, doesn't IDE intellisense and the like cover that use case? Or do you prefer to have all of that information visible all the time?
The problem is that the type it infers is not necessarily the type you want to be exposed on that export. And then it's quite possible to have a bug in implementation of the function that results in a type that's outright wrong.
Types are a subset of contracts. Contracts are best explicit at the API boundary - which is to say, on exported functions and members of exported classes.
Agreed—there are times you definitely need to specify the type. I have them every week, but still find it the exception rather than the rule, and easier to adjust for those, than adjust for the more common case.
Annotated return types are one of the single most prominent readability benefits of typed syntax. Knowing with certainty what type will be returned in a single brief glance, without the cognitive overhead of parsing function body and checking various return statements (at best: returning a variable obtained from an external source may obscure type further).
For me this also greatly improves my ability to refactor quickly and confidently (not needing to check additional places/files during refactor for edge cases in expected type).
Yes any type-related refactor mistakes should in theory be picked up at compile time, or by smart IDEs but having the context in front of you still greatly improves speed when writing.
It's also great for code reviews where grokking context is trickier and IDE goodies are typically not as rich.
IDEs are great and all, but their features should be an augmentation of the usefulness of the language, not a requirement.
I've already mentioned code reviews as a good example of contexts where we need to read code outside of an IDE regularly, but even beyond that, a language's readability shouldn't be dependent on using some specific environment to read it.
On the one hand, I'm like you. I don't want to be forced to use a specific tool to work with a programming language.
But I can't quite put my finger on why. Like, what if we just say "language Foo includes a compiler and the working environment is this IDE"? Is that wrong? It kinda feels wrong. But that's what Smalltalk does, right? Maybe the tight integration would actually be better.
The LSP pattern allows for bringing anything you’d find in an ide to other targets (like code review tools, as used by GitHub).
I used to be a language purist, but nowadays the costs of not using an ide or lsp supported tools is just too high. I’d prefer minimal tokens and abundant secondary notations provided by parsers than having to add clunky syntax myself.
I meant I used to believe that a language should be designed to be used notepad first. Now I believe it should always be usable in notepad as a fallback, but the design of the language should be heavily influenced by what capabilities IDEs/LSPs can bring.
So IDEs first but never violate the rule that it's easy to fix something in vim or notepad in a pinch.
I would argue the opposite: Omitting the return type makes refactorings significantly riskier, and code less readable in my opinion. Leaving out types should only be done in trivial, small-scope areas of code. When done on an exported, widely-used function, this smells like "write-only" code and is hard to maintain.
I don't understand this at all. Type annotations absolutely make a code base easier to maintain, not the opposite. If you are in the "types are overhead" camp, why use typescript at all?
There's a distinct difference between type annotation and type information. In the case shown above, as long as `otherFunc()` is strictly typed, the manual annotation is simply duplicating information that's already there. At no point did I argue against having types, I'm opposing needless duplication. As perhaps a simplified example, annotating `Foo` in the below example is similarly redundant:
This is over simplified. Using TypeScript you should specify all the types required to have your code base fully qualified - not more. In the example above having a fully typed `otherFunc` would directly specify the type on `func`. If you (automatically) refactor `otherFunc` the type naturally flows and you don't end up with (unnecessary) modifications on other spots. Use type inference when possible, but don't over do it.
It tells you what the function return type is, which isn't necessarily the same as the return type you might have manually typed or intended. For example, undefined's sneaking into the type because of branching code.
I think this can go both ways. There are lint rules that encourage explicit return types for exported functions and public functions, because return types are the interface between modules. As your codebases grow, having clear relationship and contracts between modules becomes more important. This goes doubly for modules distributed as NPM packages - you want the compiler to tell you when changing the body of an exported function constitutes a breaking change to your module’s API - and the easiest way to make that mistake is to change an inferred return type, and conversely the lowest-hanging fruit to prevent that error is to lint for explicit return types.
I agree. You do need to check the inferred return type (sometimes it will infer a union or new interface you didn't intend), but it helps with refactoring and encapsulation to not add those annotations.
[0] https://github.com/microsoft/TypeScript/wiki/Performance#usi...