When I first started learning TypeScript, I struggled with these concepts and still can't find any useful articles on the subject. I believe many people might have an itch about this subject, so why not write about it?
First, you must understand that this topic is subject to debate. Even the TypeScript documentation is quite vague or omits to make an in-depth comparison of these different data structures.
Most information I found was about what it is not how to choose the correct data type efficiently.
Prerequisites.
You do not need much prior knowledge of TypeScript to understand this article. The idea is to give you a general understanding of what these data structure does. Along with a few rules of thumbs that are easy to remember, you will be well on your way to mastering this more intermediate topic.
Understanding the value space and typing space.
These three data structures can exist in the same plane, although two will not even exist at runtime. Grasping this concept will probably give you a new perspective on using typings in the first place. If you are coming from other statically typed languages, this example will help you a lot:
type Cat = { name: string; purrVelocity: number };
var Cat = { slideStuffOffTheTable = true };
This is valid code that will not raise an error. Why is it so?
In TypeScript, types and values do not exist on the same Namespace. Both these tokens have no relation, and they cannot collide because TypeScript is a superset of JavaScript. It helps you find mistakes while trying to compile itself into good old JavaScript. Therefore, the type Cat will not exist at run time. The resulting code can run inside a browser — or in other cases, a barebone Node process. The expression type Cat would not be valid in plain JavaScript; therefore, as you guessed, it removes it.
Might you be wondering about classes at this point? You are right to realize that classes are valid in modern JavaScript. But then you have to understand that you can use classes as type annotations as well.
class Dog {
isDog() {
return true;
}
}
// This is fine
var dog: Dog;
// This is wrong, Syntax error 'Dog' has already been declared.
var Dog;
There you go, classes in TS exist both inside the value and typing space. In this particular case, we are merely telling TS that dog will be an object of the type Dog Here is a breakdown:
-
Classes exist in typing space and value space because they stay after the compilation and can be used to create new objects at runtime.
-
Types only exist in the naming space;
-
Interfaces only exist in the naming space.
What is the point of types and interfaces right now — I mean, everything can be a class, right? Well, yes and no.
TypeScript is a tool that helps you find syntax errors, document the data that pass around your functions, and generally gives you a better development experience than pure JavaScript.
In your .ts files, it's make-believe land. You believe you are writing in a new language, but actually, it's just adding annotations for the compiler to spot your mistakes before you try to run that code.
With a clearer understanding of this concept, let's move on to why you are here in the first place!
When should you use types in TypeScript?
Unlike classes, types do not express functionality or logic inside your application. It's best to use types when you want to describe some form of information. They can describe varying shapes of data, ranging from simple constructs like strings, arrays, and objects.
-
You want to express some form of information that you can name distinctively (example: SquareType represents a square).
-
To help TypeScript debug your code, or prevent mistakes while you are coding with your IDE help.
-
To pass it around as concise function parameters.
-
To describe a class constructor parameters.
-
To document large objects coming in or out from API's or SDK's.
-
They work best when they are small.
-
Generally, they do not hold a state nor methods;
-
When you want to express that a value can be of two different types, this is called a union — it should be possible to name this union distinctively (ex: Cator Dog union is AnimalType).
-
If you are expressing too many associations, you may want to use an enum instead.
When should you use interfaces in TypeScript
Where most people starting with TypeScript get confused is what the difference between types and interfaces is? With the most modern versions of the language, they are getting ever so slightly similar.
There are only two reasons you may want to use an interface over a type, one of which is declaration merging, while the other is a matter of coding style (or preference). Before you start screaming at me in comments, read further.
interface Point {
x: number;
}
interface Point {
y: number;
}
const point: Point = { x: 1, y: 2 };
This is perfectly valid code! If you were to use types, extending, or merging, you would have to go through the hassle of creating a new type that joins the two.
This is very useful when you write libraries that are consumed by a third-party app. An excellent example of this is express.js, a simple yet powerful package to help you create routes, controllers, and middlewares for your application.
import express, { Request, Response } from "express";
const app = express();
interface Request {
isDogRequest: boolean;
}
app.use("/dogs", (req: Request, res: Response, next) => {
req.isDogRequest = true; // <-- does not cause an error
next();
});
Very convenient! You will now have IDE autocompletion, and it will be a lot easier when you come back later to this route to understand what is going on in the req object.
Note: every time I show this to developers that create a custom type folders inside their project structure to fix this issue — their brains explode! You have been warned.
The other reason you may want to use an interface is purely from a coding style standpoint. The idea is that you can do the following with types as well — but for some reason, I feel it is more evident with this.
Imagine that you have an array of animals that can contain two (or more) types of classes. For the sake of simplicity, we will say Catand Dog. While looping that array and accessing properties, you should want to make sure that both those classes have some in common.
interface AnimalCompare {
isDog (): boolean
}
class Cat implements AnimalCompare {...};
class Dog implements AnimalCompare {...};
const animals = [
new Cat,
new Dog,
];
animals.map(animal => animal.isDog());
Interfaces do not bring methods or properties inside a class like say extend would do. It only tells the TS compiler that Cat and Dog should both have the isDog() method. When you are writing your class implementation, it will alert you if you are missing something.
This example is very rudimentary; however, in larger projects where you collaborate with a large team, it's godsent. The interface becomes some sort of contract that must be fulfilled by the classes that implement it. Now the big difference from “extending” another class is that in the case of Dog Vs. Cat, the internal logic for isDog() is probably different — however, their outcomes are the same. In this case, they should both return a boolean value.
Call me a hipster for liking interfaces. I love em'.
When should you use classes in TypeScript
In essence, classes are more straightforward in their use than types or interfaces for most.
Classes are the brick and mortar of most* TypeScript projects. They define the blueprints of an object. They express the logic, methods, and properties these objects will inherit.
In JS or TS, classes will not create a new data type in your application; you use it as object factories. When you pass around a class that you instantiated with the new keyword, you are only moving around the object that it implements.
You can use a class in a variety of paradigms; you can extend them, compose them, inject their dependencies in other classes. It's up to you to figure out the best structure for your app and stay consistent with that style. Nevertheless, this can be the subject of a whole other article!
-
If you need to change the state of an object over time;
-
If you feel this object will need methods to query or mutate its state;
-
When you want to associate behaviors with data more closely;
-
When you need to write the logic that gives functionality to your app.
-
If you only write a bunch of properties assignments in your class, you might consider using a type instead.
Did it help you?
I wrote the article I wished I had found three years ago. They can be complicated concepts to grasp at first — do not let this intimidate you. Learning TypeScript is definitively worth the struggle. Using it has been my greatest (arguably) productivity improvement for some time.
If you come from JavaScript solely, it will help you understand how other typed language works; it's a gateway drug.
Let me know in the comments if anything can be made more explicit! Thank you for reading!