Learn TypeScript with Me!
August 10, 2023
What's the motivation of writing this blog?
I've been using TypeScript for a lot of my projects, but I want to really study TypeScript in-depth to solidify my programming skills. On learning new things, I usually just watch the videos and barely take notes. This blog is specifically made for me to make sure I understand the concepts that I have learned.
This is based on Frontend Masters' TypeScript Fundamentals v3.
Why TypeScript?
It's mostly for safety and linting especially if you work in a good IDE like VS Code. It helps development by making sure things work as intended as best as it could (do checks at compile time to avoid errors at run time).
TypeScript is composed by three things: the language, the language server (basically the thing that allows VS Code to show you suggestions or warn you when something goes wrong), and the compiler.
The compiler compiles your TypeScript code into JavaScript code that browsers can run. What is cool is that you can specify the JavaScript version you want to target! For example, if you're targetting older browsers, you can compile in ES2013, if you want it to use async-await syntax, you can compile it in ES2016. You can also change the settings of how modules are compiled, i.e. change from the ES2016 modules to CommonJS modules which Node supports.
In this course, I learned some things you should do when you write TypeScript code:
(1) When you initialize variables, it is not necessary to specify the variable type. The tutor even said that he does not like explicitly stating types when it's obvious like when you write let number = 5;
(2) There are times where you need to declare the variable first without giving it a value. This is the time where you want to explicitly state what type you want it to be.
(3) Often, in development, you'll fight hurdles like where you need to generalize the types as development goes on. You do this by making making a more general declaration beforehand.
(4) Always try to explicitly state the return value of functions.
Objects, arrays, and tuples
Objects
Here are some example to kickstart your understanding on how to type objects.
If you want an object have many keys but they all have the same kind of value, you can use something called index signature.
This allows you to do stuffs like
Arrays and tuples
For arrays, it's quite simple:
If you want to make a tuple, you have to explicitly state it as a tuple.
A step back: What is type checking anyways?
In the programming world, type checking is evaluations done to see if the type of something matches a certain condition.
This could be done when you assign variables,
or when you call functions,
... and many more. That is called type evaluation.
Static typing and dynamic typing
In a statically typed programming language, type checkings are done at compile time, not run time. This is what TypeScript does.
This is different than, let's say, Python, which is a dynamically typed programming language by default:
That code above will crash after you entered a number because when my_number / 5
is evaluated at run time by Python, it says to you, "Hey, you can't give me a string and expect me to divide it by 5!"
The term duck typing you often see JavaScript called is just another name for dynamic typing.
Nominal typing and structural typing
Nominal typing refers to checks to see whether something has a type with the exact name, such as in Java.
If you put a Truck
inside the printCar
function, it will reject because it's expecting a Car
, not a Truck
.
Nominal typing is not how TypeScript behaves.
TypeScript behaves using structural typing -- it checks whether the structure is the same or not.
In the previous example,
If you try to put a Car
, Truck
, or that randomObject
to printCar
, every single one of them will be accepted because they all share the same accepted structure of Car.
Union types
A union type is essentially like an 'OR' in math - if you have Type1 | Type2
it means it can either be Type1 or Type2. It's quite simple.
The one I want to highlight is how you can use tagged union types for cleaner code. Here is an example function that you might want to write:
The function fetch
either returns an object with structure {name: string}
or an Error
. While this is fine, processing it would be hard, i.e. you'd have to do something like this:
The portion, x instanceof Error
is what you can call as a type guard.
You can make the code more readable by introducing tags and returning it as a tuple with the tag on the 0th index.
Intersection types
Here's where things get quite interesting. Intersections might confuse you, but it's helpful to think it as a venn diagram where the intersection is a place where both properties exists.
So, if you have, say,
and
The type, A & B
(intersection of A and B) would be
because it contains both properties! (name from A and age from B)
Interfaces
In TypeScript, there are also interfaces. Sure, you can use them like a type, but it's much more strict. You cannot use things like union types or intersection types, you can only define an interface as an object.
Having an interface is useful if you want give a 'contract' to classes to make sure they implement something.
You can have two definitions of an interface, too. It will work 'like' an intersection type.
For types, this will crash:
But, for intersections, it will not!
When to use types or interfaces?
Use types if you want to use union/intersection types.
Use interfaces if you want to 'create contracts' for classes or you want the users of your code to be able to add properties to it.
Function types
You can define the signature of a callable type (i.e. functions) using this kind of syntax:
The return type void
In JavaScript, we know that functions that returns nothing will return undefined
.
In TypeScript, if you want to accept that a function you expect to not return anything, do not tell that it returns undefined, instead say that it returns void.
Returning undefined means that it will expect you to return undefined specifically but returning void means to ignore whatever the return type is.
Example:
If you pass a handler that returns something other than undefined, handleNumber
will work just fine, but handleNumber2
will output an error.
Function overloading
This is a bit complex, but let's say you have a scenario like this:
Say, you want the action to be NumberHandler
only if x is a number and StringHandler
only if x is a string.
To make this possible, you need something called function overloading.
One thing to make sure is that the signatures in the implementation must accommodate all the signatures you specified when you overload it.
Working with classes
There is a problem in JavaScript where you can't really know what things you have in a class:
Using TypeScript, you can make it clearer!
Access modifiers
I assume you readers have learnt in your OOP class about these three fields: public, private, and protected.
TypeScript allows you to do that -- but bear in mind that this is only works in the linter world, at runtime it's still pretty much exposed.
Shorthand for constructing attributes
To avoid writing a lot of duplicate this.<var_name>
assignments, you can do things like this in TypeScript:
Readonly attributes
You can define something that behaves like a constant defined using const
for a class with readonly
, i.e.
Top values
Let's discuss about these two types: any
and unknown
. They're similar in a way that they accept any value.
The difference happens when you try to actually use the variable.
You actually have to use type guards when using unknown
, i.e.
When to use them?
Generally, you'd use any
in situations where you are moving from JavaScript to TypeScript incrementally. You'll move gradually from the easier types first and leave the harder types on any
and work on it incrementally.
unknown
, meanwhile is quite useful when you get a return response, say from an API. It could be anything (i.e. an error response or a successful response), so you'd want to check it first with type guards before processing it.
Bottom values
There is a type called never
in TypeScript. It means that value cannot hold anything. Why is this useful?
Bottom values are useful when you absolutely want to check if your conditionals are exhaustive. Let's take a look at this example.
In the last block, neverData
will have a type of never
. How is this useful?
Let's say as your platform grows, you might add more vehicles later, e.g. you'll change the type Vehicle
to Car | Truck | Boat
:
This way, TypeScript helps you to handle cases that you might've forgotten when you add new stuffs along the way.
Type guards
There's a lot of way you can check for types, i.e.
But say, you have a more complex type and want to create a function that checks for that certain type. There's two ways to do this.
Using is
You can assure TypeScript that something is a type of something by using the keyword is
:
(yes, the checking is a bit tedious, but we've to deal with it)
Doing it this way, you must be absolutely sure that what you write inside the condition is right.
Using asserts is
Using is
doesn't guarantee you are free from runtime errors. At times, you might want to throw errors if the actual thing is not a person at runtime. You can use this keyword, i.e.
Nullish values
You can use the !.
operator to tell TypeScript that you're certain that something has the property of something. Let's take a look at an example.
It's not a good idea to do this, though. You'd probably better off with something like this:
Generics
Use generics on cases where you want a function to accept anything and return anything without you losing the helpful type inference. Let's take a look at an example where you want to create a function that transforms a list of something into dictionaries.
There's just one problem: you're having a lot of any
here. When you access dict.carId.maker
you won't be warned because it assumes that dict.carId
is a type of any
, not a type of Car
.
Generics comes to save your day! Here's an example how you'd use them:
Now, when you try to access, let's say, carDict[1].
, you'll be presented with only [id, year, name]
to select. It now sees that each key holds a type of Car!
The <T>
is what you'd call a type parameter. It allows you to parameterize any type you'd like to accept so it doesn't lose it's typing at the end (unlike when you use any
).
Giving constraints to your generic
Let's say you want for sure to be certain that you only want to accept all types that have the property id
in it. In other words, you want to make sure that T has a property of id
in it.
To do so, you can use the extends
keyword for your type parameter!
Now, when you access T
, you can be sure that T
will have an id
in it -- no more need to create an idSetter function like in other examples!