by Preethi Kasireddy
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 four-part series is about.
First, a definition
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 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
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
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!
Part 1: A quick intro to Flow syntax and language
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.
This describes a
Notice that when you want to specify a type, the syntax you use is:
This describes a string.
This describes the
This describes the
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
Array<;T> to describe an array whose elements are of some
Notice how I replaced
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
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
You may get an error:
To circumvent this, you can use
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
amountExceedsPurchaseLimit is annotated as also returning a
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
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
By doing this, you’re indicating that
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 was of type
number. Meanwhile, for
arrayT, T was of type
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
undefined value. They have the type
T|void|null for some type
T, meaning it is either type
T or it is
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
You can also use maybe to indicate that an object property will be either of some type
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
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’re done with syntax
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 & 3.