Why am I sharing my travel stories?
Founder & CEO of TruStory. I have a passion for understanding things at a fundamental level and sharing it as clearly as possible.
Well it turns out learning types isn’t just an exercise in mind-expansion. If you’re willing to invest some time in learning about static types’ advantages, disadvantages, and use cases, it could help your programming immensely.
Interested? Well you’re in luck — that’s what the rest of this 3-part series is about.
The quickest way to understand static types is to contrast them with dynamic types. A language with static types is referred to as a statically-typed language. On the other hand, a language with dynamic types is referred to as a dynamically-typed language.
The core difference is that statically-typed languages perform type checking at compile time, while dynamically-typed languages perform type checking at runtime.
This leaves one more concept for you to tackle: what does “type-checking” mean?
“Types” refers to the type of data being defined.
For example, in Java if you define a boolean as:
boolean result = true;
This has a correct type, because the boolean annotation matches the value given to result, as opposed to an integer or anything else.
On the other hand, if you tried to declare:
boolean result = 123;
…this would fail to compile because it has an incorrect type. It explicitly marks result as a boolean, but then defines it as the integer 123.
var result = true;
So as you can see, types allow you to specify program invariants, or the logical assertions and conditions under which the program will execute.
Type-checking verifies and enforces that the type of a construct (constant, boolean, number, variable, array, object) matches an invariant that you’ve specified. You might, for example, specify that “this function always returns a string.” When the program runs, you can safely assume that it will return a string.
The differences between static type checking and dynamic type checking matter most when a type error occurs. In a statically-typed language, type errors occur during the compilation step, that is, at compile time. In dynamically-typed languages, the errors occur only once the program is executed. That is, at runtime.
On the other hand, if a program written in a statically-typed language (like Scala or C++) contains type errors, it will fail to compile until the errors have been fixed.
In either case, when you want to use types, you explicitly tell the tool about which file(s) to type-check. For TypeScript you do this by writing files with the .ts extension instead of .js. For Flow, you include a comment on top of the file with @flow
Once you’ve declared that you want to type-check a file, you get to use their respective syntax for defining types. One distinction to make between the two tools is that Flow is a type “checker” and not a compiler. TypeScript, on the other hand, is a compiler.
Personally, I’ve learned so much by using types in my day-to-day. Which is why I hope you’ll join me on this short and sweet journey into static types.
The rest of this 4-part post will cover:
Note that I chose Flow over TypeScript in the examples in this post because of my familiarity with it. For your own purposes, please do research and pick the right tool for you. TypeScript is also fantastic.
Without further ado, let’s begin!
To understand the advantages and disadvantages of static types, you should first get a basic understanding of the syntax for static types using Flow. If you’ve never worked in a statically-typed language before, the syntax might take a little while to get used to.
Notice that when you want to specify a type, the syntax you use is:
number includes Infinity and NaN.
This describes a string.
Note that null and undefined are treated differently. If you tried to do:
Flow would throw an error because the type void is supposed to be of type undefined which is not the same as the type null.
Notice how I replaced T with string, which means I’m declaring messages as an array of strings.
You could add types to describe the shape of an object:
You could define objects as maps where you map a string to some value:
You could also define an object as an Object type:
This last approach lets us set any key and value on your object without restriction, so it doesn’t really add much value as far as type-checking is concerned.
This can represent literally any type. The any type is effectively unchecked, so you should try to avoid using it unless absolutely necessary (like when you need to opt out of type checking or need an escape hatch).
One situation you might find any useful for is when using an external library that extends another system’s prototypes (like Object.prototype).
For example, if you are using a library that extends Object.prototype with a doSomething property:
You may get an error:
To circumvent this, you can use any:
The most common way to add types to functions is to add types to it’s input arguments and (when relevant) the return value:
You can even add types to async functions (see below) and generators:
Notice how our second parameter getPurchaseLimit is annotated as a function that returns a Promise. And amountExceedsPurchaseLimit is annotated as also returning a Promise.
Type aliasing is one of my favorite ways to use static types. They allow you to use existing types (number, string, etc.) to compose new types:
Above, I created a new type called PaymentMethod which has properties that are comprised of number and string types.
Now if you want to use the PaymentMethod type, you can do:
You can also create type aliases for any primitive by wrapping the underlying type inside another type. For example, if you want to type alias a Name and EmailAddress:
By doing this, you’re indicating that Name and Email are distinct things, not just strings. Since a name and email aren’t really interchangeable, doing this prevents you from accidentally mixing them up.
Generics are a way to abstract over the types themselves. What does this mean?
Let’s take a look:
I created an abstraction for the type T. Now you can use whatever type you want to represent T. For numberT, T was of type number. Meanwhile, for arrayT, T was of type Array<number>.
Yes, I know. It’s dizzying stuff if this is the first time you’re looking at types. I promise the “gentle” intro is almost over!
Maybe type allows us to type annotate a potentially null or undefined value. They have the type T|void|null for some type T, meaning it is either type T or it is undefined or null. To define a maybe type, you put a question mark in front of the type definition:
Here I’m saying that message is either a string, or it’s null or undefined.
You can also use maybe to indicate that an object property will be either of some type T or undefined:
By putting the ? next to the property name for middleInitial, you can indicate that this field is optional.
This is another powerful way to model data. Disjoint unions are useful when you have a program that needs to deal with different kinds of data all at once. In other words, the shape of the data can be different based on the situation.
Extending on the PaymentMethod type from our earlier generics example, let’s say that you have an app where users can have one of three types of payment methods. In this case, you can do something like:
Then you can define your PaymentMethod type as a disjoint union with three cases.
Payment method now can only ever be one of these three shapes. The property type is the property that makes the union type “disjoint”.
You’ll see more practical examples of disjoint union types later in part II.
All right, almost done. There are a couple other features of Flow worth mentioning before concluding this intro:
1) Type inference: Flow uses type inference where possible. Type inference kicks in when the type checker can automatically deduce the data type of an expression. This helps avoid excessive annotation.
For example, you can write:
Even though this Class doesn’t have types, Flow can adequately type check it:
Here I’ve tried to define area as a string, but in the Rectangle class definition we defined width and height as numbers. So based on the function definition for area, it must be return a number. Even though I didn’t explicitly define types for the area function, Flow caught the error.
One thing to note is that the Flow maintainers recommend that if you were exporting this class definition, you’d want to add explicit type definitions to make it easier to find the cause of errors when the class is not used in a local context.
2) Dynamic type tests: What this basically means is that Flow has logic to determine what the the type of a value will be at runtime and so is able to use that knowledge when performing static analysis. They become useful in situations like when Flow throws an error but you need to convince flow that what you’re doing is right.
I won’t go into too much detail because it’s more of an advanced feature that I hope to write about separately, but if you want to learn more, it’s worth reading through the docs.
We covered a lot of ground in one section! I hope this high-level overview has been helpful and manageable. If you’re curious to go deeper, I encourage you to dive into the well-written docs and explore.
With syntax out of the way, let’s finally get to the fun part: exploring the advantages and disadvantages of using types!
Next up: Part 2