I am the only one who is happy to code without static typing? Enjoying fast compilation, small, fast editors, flexibility.
I don't really make stuff like marry(a, b) and then call it with a banana and an apple by accident. Sure I make plenty of mistakes when I am coding but only very few could have been caught by a static type check.
Take one of the examples in the slides:
let obj: string;
obj = 'yo';
// Error: Type 'number' is not assignable to type 'string'.
obj = 10;
I simply don't write code like that. The crucial difference is that I would pick a variable name that made it obvious what goes in it:
let text = 'yo';
// This line here - I would not in practice write as 10 obviously does not belong in text:
text = 10;
(similarly I would never use the variable name 'what')
Primitive types such as string/number/... are really not a great example when it comes to showcasing the benefits of static typing. I don't know why it's so common.
A better example would be something like
// let's pretend the values are not dummy values, etc.
function getWeather(encodedWeatherString) {
return { temperature: 50, windDirection: 'east' }
}
function getWeatherFromUrl(url) {
return { temperature: 20, windDirection: 'east' }
}
All is well. Some time later, someone decides to add some extra data to getWeatherFromUrl, but not to getWeather, and then tries to use this extra data in some place, i.e.:
function getWeatherFromUrl(url) {
return { temperature: 20, windDirection: 'east', time: new Date() }
}
function displayWeather(weather) {
return "It was " + weather.temperature + " on " + dayOfWeek(weather.time.getDay())
}
Which will cause an error when called on the weather object returned from getWeather(), because the time property will be undefined.
This is of course a tiny tiny example where it is trivial to keep track of all the data structures in your head. But even in projects that are not that huge, it can get pretty inconvenient and mentally draining very fast. It's a lot more enjoyable to not have to deal with that kind of stuff, and just have one Weather structure defined for the whole project that everything has to adhere to and that everything can rely on.
Types are invaluable when refactoring your code. I have found Typescript to be invaluable when doing exploratory programming as it allows me to refactor heavily as I learn more about the structure of the problem. The compiler points all the affected code sites and I have reasonable confidence in the correctness of the code after the refactor.
Most static type systems really suck and I can fully understand why dynamic-coders don't like them.
It often feels like they are holding you back more than they help.
I also got such a moment with TypeScript.
const a = A()
a.b = new SubtypeOfB()
a.b.attributeOfSubtypeOfB = 123 //error
Because a.b is type B and not SubtypeOfB and so doesn't have the attribute of SubtypeOfB, but a.b gets its typing from A, which can't be changed on the fly later.
The solution was:
const a = A()
const b = new SubtypeOfB()
b.attributeOfSubtypeOfB = 123 //here TS thinks it is SubtypeOfB
a.b = b //now TS thinks it's B
Which felt like "Why do I have to restructure my code with extra variables when I clearly know that this object HAS that attribute?!"
And Java was full of those problems. Numerous different definitions of the same thing that aren't compatible with each other. Hell, some languages even have different string types.
But yeah, TypeScript is generally chill about most things and the hints it gives me were often right and helped to find the right methods without consulting any docs, which is a huge win, even without the refactoring stuff.
You can also use 'type assertions' to minimise restructuring, eg, the following is the same number of lines as your first example (and compiles to identical code):
const a = A()
a.b = new SubtypeOfB()
(<SubtypeOfB> a.b).attributeOfSubtypeOfB = 123 //works
Things change when you're working on a 1-10M LOC codebase , with 1-10K other engineers, like big companies do. Eg. Flow was written at Facebook (where I work).
How big are the projects you are working on though? For instance I work an a 2 year old Angular project and static typing is something we are trying to introduce because the codebase is large and challenging to work with, mostly because Javascript. There's only so much you can do with best practices and code organisation.
I've worked in multi-million lines mono-repo Javascript projects built with Backbone and old school ES3 (supporting IE6, etc).
Types would have helped, a little. But really, architectural soundness, proper unit tests, and frequent refactoring (using the tests) kept things under control fine.
The biggest trick is that 99% of the community has never seen/worked with a properly unit tested code base. Even though who think they did, often didn't (they have 80-95% test coverage with bad tests, overlapping tests, or integration tests...and that's blah if that's all you have).
The current JS type systems only take you so far, too: since typing is optional (for good reasons), and type definitions of 3rd party libs are often out of sync, or wrong, etc, a small error, or a missing definition, and your right click -> rename ends up breaking something subtle. It helps, but it's not enough to have full confidence in a large system. In a small app it's not really needed, and in a massive app you have the above issue, so it's mainly in the mid range people find the most benefits
For a type system to really save your butt in a large app, you need an advanced type system, like Elm/Haskell/Scala/whatever. Otherwise, the tool you actually need is proper unit tests. Types will help, a little. But they're not the panacea. Especially optional ones.
I work on / have worked on fairly big projects. Plenty of them have been "a challenge to work with". Always because of the code that had been written (+ other things). Never because of the language.
For me the trick to big projects is strong modularisation and well-defined interfaces which you can do regardless if you have static type checks or not.
Plus. I think that there is a bit of developer-psychology-101 here.
Most developers would rather go: This project sucks. This shitty language has not got a proper type system. Let's build a compiler/type-checker/use-this-cool-alpha-from-bingo-banana-I-found-last-night.
Rather than: This project sucks. I cannot believe how much bad, disorganised code we have written. Let's sit down and look at our mess to see if we can improve the way we write code, work together and in general develop way we make software.
I don't see these as mutually exclusive. Introducing a compiler/type-checker can be (depending on your situation) a very concrete way of improving the way code is written.
Which is more productive, which yields higher quality?
I have build plenty of software both ways. And my answer is a confident "it depends". But I will tell you this: A fine-grained type hierarchy over either Markdown or HTML opens up a very big can of worms.
All this reminds me of SOAP vs. HTTP/JSON: I have seen plenty of SOAP webservices seriously stuffed up regardless of/because of strong typing whereas developers seem to do much better with HTTP/JSON because of the simplicity and directness of the approach.
You're not the only one. It's a common sentiment if you haven't really, fully used a type system before.
If there's an important property you want your program to have, and you can express it using the type system, then your compiler can prove it for you. For example, if you have an Employee record with a jobTitle field of type String, then it's hard to be certain you'll never get an invalid jobTitle. If you change that to a type hierarchy with subtypes like Manager, Developer, etc. then once the program compiles, you can be certain that particular error can't happen at run time. As type systems grow more sophisticated, more program properties can be represented as types.
If you want to experience it, perhaps try a language with a really top-notch type system like OCaml or Haskell. Maybe try implementing some kind of transpiler from json to json that uses the type system to describe the valid input. You'll have a collection of parsing functions that detect all the errors, and then the rest of the system can rest assured that the parsed representation is error-free, and you can be certain that you never forgot to handle one of the possible cases. (OCaml, in particular, is very good at this.)
If you try this, and you're human, I think you'll make a few mistakes that get caught by the type checker.
Every time I have to wade into a legacy dynamic codebase I find all kinds of type errors that aren't caught by tests. That's bad enough on its own but refactoring without support from a type checker is a pure high wire act.
I agree I don't miss types too much when the amount of code is small. However, when you have thousands of lines of code including code you didn't write and JavaScript spits out a "undefined is not a function" error in the middle of running a complex program, static typing can save you a huge amount of time and sanity.
Also, what are you losing by introducing strong types? Why wouldn't you want the compiler to verify the logic you're keeping track of manually?
Next time you spend hours looking for a strange bug that turned out to be a honest-to-god misspelling, think about how you wouldn't have had that bug in the first place if you were using static typing.
I feel like people don't like admitting that this happens to them, but when you're at the end of a long day, it's so easy to read your typo as if it were correctly spelled.
Like the presentation said, if you have a small project then don't use type checking. If you work in a big project with a lot of code that you didn't write then it could help a lot.
Also you don't have to use it everywhere with Flow.
I used to be like that, but after a lot of JS programming I ended up with WebStorm and very heavily using JSDoc and Closure compiler syntax inline type-comments - which together with WebStorm give me almost TypeScript-like type checks. There is not a variable or function parameter declaration without a type in my code now.
Example (the inline type info is Closure compiler syntax, with the "@" it's JSDoc):
/**
* @typedef {Object} MyResultType
* @property {string} example
* @property {Function} exampleFn
*/
const /**string[]*/ myPromise = Promise.resolve(['','']);
const /**MyResultType*/ t = myPromise.then(
/**string[]*/ a => a.map(/**string*/ s => s.toUpperCase())
);
Or a more lengthy JSDoc comment when I need to document a variable anyway:
/**
* This is a very important and beautiful variable
* @type string[]
*/
const myPromise = Promise.resolve(['','']);
I have a lot of complex return types - meaning an object instead of a simple type, because a function can only return one value and I often have to return more than that. I also have lots of promises (also as return values), and it's easy to get confused which function returns a result synchronously and which one asynchronously.
Sync. vs. async. isn't something logical and obvious - it's determined by where you get data from. No coding construct won't change that we have to keep the hardware in mind and concentrate on just the algorithm when we don't have blocking function calls, "yield" and "async" don't change that, we have to remember to add them. So having difficulty remembering which function returns the result this or that way has nothing to do with the algorithm and I appreciate the IDE (in my case, or TypeScript in others) keeping track of the types.
Even though I don't use TypeScript I use TypeScript "DefinitelyTyped" interface definitions for things like Node and Mocha - WebStorm uses them for its behind the scenes type-checking.
By religiously always adding a type even when it seems obvious I get WebStorm to report when a type doesn't match what it determined should be there, and it's meta-information for myself. It always ensures that I always get correct auto-completion suggestions and inspection warnings - because the IDE isn't always capable of resolving all variables. Especially when it's promises created in another module and after the n-th chain element and in connection with arrow functions and parameter destructuring the IDE often gives up, there still are gaps in its ability to find the intended type since those constructs are too new. However, each time I find a hole I file a bug report and the JetBrains team almost fixes it pretty quickly for the next version. "Flow" is supported too. Example for the type of bugs one may find, and how a type inspection error looks like (randomly picked from the bug list): https://youtrack.jetbrains.com/issue/WEB-21839#u=14643829544... (that bug has been fixed)
I did a little bit of TypeScript, WebStorm support is very good there too. I try to avoid adding a compilation step but it sure looks tempting - and just a year or two ago I was adamantly against it as "useless". Well, my attitude has changed after having fond types very convenient on a large exploratory project where I often have to revisit and refactor and often completely rewrite previously written code and/or restructure large sections and multiple modules at a time.
I'm not saying you absolutely must use types, it's just that many people incl. myself find it more convenient, and in my case I even was in the other camp for the longest time.
I don't really make stuff like marry(a, b) and then call it with a banana and an apple by accident. Sure I make plenty of mistakes when I am coding but only very few could have been caught by a static type check.
Take one of the examples in the slides:
I simply don't write code like that. The crucial difference is that I would pick a variable name that made it obvious what goes in it: (similarly I would never use the variable name 'what')