[Typescript OOP 3/3] Type safety with Interfaces, types, and enums
This is the last post of a 3-post series where we learn about type safety in Typescript to ensure things break at Compile time if a typing goes wrong!
What is type safety?
Type safety is an abstract construct we create on our code where we tell our coding and compiling software to display errors (a way of telling us that there is something wrong in the program which can break later) if a few things do not run as they should. With types, it means that if we want to store only numbers in a variable, and we mistakenly use the same variable to store a boolean or a string data, our compiling software, and our development environment will let us know that "hey, what you did here is wrong. You are typing a string data in a number based variable". What this helps us in is preventing those errors before we release our product.
Compile-time vs Run-time
You might have encountered these two terminologies across various discussions on errors and performances. So, what are these?
Compile-time
Compile-time points to the time when the software is in development and is not being executed at the moment. Compile time correctness means having correct data flow in principle where everything in the scope of the software is correct. This means there can be no flaw as far as internal working is concerned. However, there is no guarantee that things will be smooth if there are external variables involved.
What this means is, if your program does not take any inputs or interact with any other data outside of what you hard-code in, your software can NEVER go wrong. It will forever work without errors. However, this is not how most software is written or used. We often need to make use of external data like files, user inputs, camera data readings, etc. In this case, we can only have a guess at what type of data we may feed our program and therefore our software is only good as the data that it gets.
For eg.
let marks: number = 10;
if (condition1 == true)
marks = "Passed"
console.log("Percentage is" + (marks/100))
// This will lead to a compile time error as marks can only be typed as number and not string.
As we see in the example above, any such kind of unmistakenly developer error can easily be found on compile time as it is definitely wrong. The logic there is wrong as the variable "marks" can later be used for other purposes where the assumption is that the variable we receive there is a number.
Run-time
Moving further in the process of error catching from compile-time, we arrive at run-time. This means that every local variable or process whose data we are sure of, can not fail at any point. However, any external data-related values might. Take for an instance, a chance where we take the marks value from user input instead of a hardcoded data.
let marks: number = getFromUserInput();
console.log("Percentage is" + (marks/100))
// This will pass in compile time as we assume that the the value that the user enters will be correct. However, it will fail if the user-entered value is not a number.
As we see in the example above, all external data can be corrupted or not in the specified type that we may need it to be. For example, we may expect that an API request to the current weather will be in the format of an object containing name, temperature, and comments as keys. But, the values in the API responses have now changed to send the keys as place, temp, and comments. So, our quest to check for the name value of the object will definitely go wrong when the software is actually run and not anytime before because our software can never know anything about the other software or server until we tell it about them.
Hence, we bring a lot of compile-time and run-time checks into our software that we are going to learn today.
Interfaces
Interfaces in typescript actually represent the structure of an object. Interfaces describe the skeleton of the object we are working with. We know what data types number, string, and boolean are. We also know the arrays construct that will contain an ordered list with values at every index of the array. However, objects are trickier as they do not have a fixed key. There is nothing fixed in an object except for its { key: value }
notation. There can be any level of nesting inside an object. Hence, we deal with them by creating our expected custom data type beforehand which we call interface.
interface WeatherData
{
name: string;
temperature: number;
correctness:
{
accuracy: number;
precision: number;
}
}
let myWeather: WeatherData = getResponseFromWeatherServer();
console.log(`There is a ${myWeather.correctness.accuracy*100}% chance that ${myWeather.name} will see temperatures of ${myWeather.temperature} ± ${myWeather.correctness.precision}`);
// for a response of
// {
// name: "Berlin",
// temperature: 6.7,
// correctness:
// {
// accuracy: 0.7,
// precision: 0.1
// }
// }
//
// our software will log
// There is a 70% chance that Berlin will see temparatures of 6.7 ± 0.1
What interfaces also do is, help our IDE (most commonly used Integrated Development Environment or IDE is Visual Studio Code) to help us write the correct spelling of keys and object names. They list out the entire gamut of possible accessors from our code for a given object.
This is because we have defined the structure of our mystery object as such and our IDE helps us not mistype our values. Also, it helps us to not type the entire names by availing us the option of selecting our desired key with arrow keys or mouse.
What we can also do with Interfaces is extend them or use within other interfaces.
Let's see an example below
interface Person
{
name: string;
age: number;
}
interface SchoolDetails
{
name: string;
phoneNumber: number;
}
// SchoolStudent has all properties of Person but also extra key called school which holds value in the form of SchoolDetails described above.
interface SchoolStudent extends Person
{
school: SchoolDetails
}
let something: SchoolStudent =
{
school:
{
name: "BDMI",
phoneNumber: 23424523432
},
name: "",
age: 0
}
So, that was interfaces. Now, let's see what are types.
Types
Types are aliases that we use to describe certain data types or structures in our code. Hence, it is quite similar to Interfaces when it comes to defining structures of objects. But it has far more capabilities and use cases too.
Let's look at the previous code example using types.
type Person =
{
name: string;
age: number;
}
// Describe PhoneNumber type as similar to a number type.
// Use that instead for phone number now in SchoolDetails
type PhoneNumber = number;
type SchoolDetails =
{
name: string;
phoneNumber: PhoneNumber;
}
// SchoolStudent has all properties of Person but also extra key called school which holds value in the form of SchoolDetails described above.
type SchoolStudent = Person &
{
school: SchoolDetails
}
let something: SchoolStudent =
{
school:
{
name: "BDMI",
phoneNumber: 23424523432
},
name: "",
age: 0
}
Notice 3 things above:
Types are used in a similar way as interfaces except that types are assigned using an "=" symbol where as interfaces are defined without one.
Types can extend other types using algebra symbols
Types can be used as an alias for native data types. This is useful if the data type might later change to something else and we can deal with it by simply editing the type declaration.
Some more ways of using types are as below:
As you can see, types help us in alleviating typing mistakes and also grants us predictions that allow us to select the appropriate option that a variable can be set to for a given type. 0.02 is fine but 0.07 is not and our IDE lets us know of it while writing code.
This similar tool for helping us in setting only a few permitted values can also be accomplished using enums. Let's find out what they are:
Enums
Enums, short for Enumerations, is a list of values compiled in an alias type.
It helps us in 2 ways:
Value prediction by displaying a list of allowed options to set from
Type Safety of showing error if the selected value does not exist on an enum
// Enum keys cannot start with numeric values
enum eVersion
{
_0_01,
_0_02,
_0_03
}
let myVersion: eVersion = eVersion._0_02
// value of myVersion = 1
We get similar type safety here too.
However, there is one difference between the two implementations.
Enums in general are number enums. What this means is they do not have the same value as you see in the list. They usually are numbered as array indices.
enum eVersion
{
_0_01, // 0
_0_02, // 1
_0_03 // 2
}
//////// OR /////////
enum eVersion
{
_0_01, // 0
_0_02 = 10, // 10
_0_03 // 11
}
We can manually set different number values for each enum key but at the end of the day, they remain numbers.
Unless, we set them as string copies of their keys.
enum eVersion
{
_0_01 = "0.01",
_0_02 = "0.02",
_0_03 = "0.03"
}
let myVersion: eVersion = eVersion._0_02
// value of myVersion = "0.02"
Now, the value in myVersion will be set as "0.02" as was the case when we were using types.
So, this is how we can ensure type-safety in our code to ensure we do not mistype or use variables of a different type to store our values. These are the causes that will break your software before you can ship them as they fail during compilation. Compilation is a good thing as it adds a step in our release process. Without this, you would have a tough time ensuring quality as Javascript does not have any type safety and it is extremely easy to break stuff there. However, by using Typescript, we can bring order and type-safety within the Javascript code while developing to ensure we are safer than before.
Thank You! 😄