What is TypeScript?
TypeScript is a programming language that is built on top of JavaScript, and it adds additional syntax support. According to W3 Schools, the main advantage of using TypeScript is that it can enable developers to specify the types of data that are being used throughout our code. However, what does this actually mean? Let's get into that with some examples in our next section.
Why is TypeScript useful?
JavaScript
const x = 5;
const y = 2;
function add(x, y){
return x + y;
}
In the above example, the developer knows that in order to use this function you should be passing in numerical (Number, Integer, Float) data types in order for the code to execute correctly. However, there currently doesn't exist any safeguards that prevent misuse of this function. Although there are error messages, the error you receive might not be informative.
const x = 'Cat';
const y = 2;
function add(x, y){
return x + y;
}
console.log(add(x, y)) // => outputs 'Cat2' rather than a number
If we wanted to restrict how this function could be used in native JavaScript, we might consider implementing exception and error handling. A basic implementation might look something akin to the following.
const x = 'Cat';
const y = 2;
function add(x, y){
const typeIsValidX = typeof x === 'number'
const typeIsValidY = typeof y === 'number'
if (typeIsValidX && typeIsValidY){
return x + y;
} else {
console.error(`Expected type Number for arguments in function add().`)
}
}
console.log(add(x, y)) // => causes error message (below)
// "Expected type Number for arguments in function add()."
In the previous example, we have now created a simple check to see if the arguments provided to our add()
function are numbers. If the arguments are indeed numbers, then we receive our result. If we have provided invalid arguments, we receive an error message instead. This is good, but our error message is not telling us what data type we have provided for our arguments. How can we add additional error handling so that this is more informative?
function add(...args){
const errMsg = args.reduce((accum, curr, index) => {
if (typeof curr !== 'number'){
accum.push(`Invalid argument at args[${index}]; expected type '${typeof curr}' to be type 'number'.`);
return accum;
} else {
return accum;
}
}, [])
if (errMsg.length === 0){
return args.reduce((accum, curr) => {
return accum + curr;
}, 0)
} else {
errMsg.forEach((error) => {
console.error(error);
})
throw new TypeError(`Invalid arguments provided to function; expected all arguments to be of type number.`)
}
}
Now we have error handling within our function, where we are asserting that arguments must be of a certain type in order for the function to execute. We are also being informative about what we are doing wrong with our function (which variable is not meeting a type expectation). This seems a little over-the-top, but if our add()
function took an unlimited number of parameters this could be handy for diagnosing errors; this error handling could also be useful if we are using the add()
function in multiple places.
So what does this have to do with TypeScript? All of the code that we have written above could be handled with TypeScript, which enables us to restrict a variable (or function) to using certain data types. So what does this look like with TypeScript?
TypeScript
let y: number = 2;
let x: number = 'Cat'; // => causes error message(below)
// Type 'string' is not assignable to type 'number'.
In the above variable declarations, we are declaring two variables x
and y
. Notice that we are also including number
following a colon; this is how TypeScript can 'assign' a data type for that variable to use (more information on basic types here). Because we have assigned the data type of number
to the variable x
, TypeScript will throw an error to let us know that we are not providing a valid data type.
Now, what about our function? In the previous example we were only attempting to declare and initialize a variable, but how can we restrict a function to only accept arguments of a certain data type? The execution here is very similar.
function add(x: number, y: number): number {
return x + y;
}
console.log(add('Cat', 7)); // => causes error message (below)
// Argument of type 'string' is not assignable to parameter of type 'number'.
This is one of the key features of TypeScript; it's in the name! As a language TypeScript is helping us to manage the types of data that we are expecting throughout our code. This is especially valuable for error handling, but it's also beneficial for scalability.
What else should I know?
According to the official documentation: TypeScript knows the JavaScript language and will generate types for you in many cases. For example in creating a variable and assigning it to a particular value, TypeScript will use the value as its type.
let user = {
name: "Hayes",
id: 0,
};
user = 'Cat'; // causes error message (below)
// Type 'string' is not assignable to type '{ name: string; id: number; }'.
This also gets more complex with variables that are objects. In the previous example, you might have noticed that the error message states not assignable to type '{ name: string; id: number; }
. This means that even if we were to have assigned user
to an empty object {}
we would also get an error. However, why does this occur? Let's look into this in our next section.
Interfaces & Data Structures
TypeScript introduces an interface
keyword, which allows for you to create a data structure that adheres to certain constraints; this is similar to instancing an Object with a Class but the relationship extends beyond that. In the example from the previous section, it is implied that the user
belongs to an interface. If we were to alter the code above to reflect how TypeScript is viewing it, it would look more akin to the following example.
// In the background, TypeScript is implicitly developing this interface
interface User {
name: string;
id: number;
}
// Here we see that the user variable is referencing the interface on assignment
const user: User = {
name: "Hayes",
id: 0,
};
In TypeScript, an interface defines the shape of an object by specifying its properties and their types, but without implementing any functionality. According to the official documentation, an interface 'serves as a contract', ensuring that any object or class that implements the interface adheres to its defined structure. Interfaces allow for strong typing and code consistency, making it easier to catch errors at compile time and enabling better code documentation and auto-completion support.
Do Interfaces make Classes redundant?
As you review the previous code example, you might begin to question whether or not TypeScript makes classes redundant. In a similar, but distinct way, both a TypeScript interface and native classes enforce a data structure. However, despite these similarities, there are differences between how they are used in our code. We are going to expand on the example from the previous section here, and we are going to use the interface with a class.
interface User {
name: string;
id: number;
}
class UserAccount {
name: string;
id: number;
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
}
let user: User = new UserAccount("Justin", 1);
At first glance this can be somewhat overwhelming and confusing to look at, so we are going to break this down into parts so that we can get a better understanding of what each piece of code is responsible for.
interface User {
name: string;
id: number;
}
The above code example is creating an interface that other variables in our application will adhere to. It is essentially establishing a template that variables in our code must follow, which is useful for ensuring that we are working with the proper data type and structure when we attempt to execute a set of code. However, it is important to note that this interface is only used at compile time.
class UserAccount {
name: string; // TypeScript expects explicit property declarations
id: number;
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
}
In the above code, we are declaring a class that expects for there to be a name
and id
property that will be used in a constructor to instance a new Object. It is important to note that this Class is used at runtime to create new instances of an Object; at compile time the previously covered interface enforces that a variable will follow the structure returned by the Class.
let user: User = new UserAccount("Justin", 1);
Finally, we are stating the for the variable user
it will adhere to the User
interface that we have defined earlier. On compile time, it is expected that this user
variable will henceforth follow this data type and structure. On runtime, our Class is used to create (and return) a new instance of an Object...and our User
interface expects certain values to be returned by that class.
How can I get started using TypeScript?
There are three ways that you can get started with TypeScript; according to the official documentation the three most common installation routes are via an npm module
, NuGet package
, or by installing it as a Visual Studio extension
.
If you would like to learn how to use TypeScript or understand it further, please consider reading through its extensive documentation on its website.
Conclusion
TypeScript can be a valuable addition to your development environment, as it can implement additional error handling and it can help you better understand errors in your application when they occur. Learning and implementing TypeScript can present challenges that might initially slow the development process. However, once you have a better understanding of TypeScript, it can make the development process easier for you or your team!
Top comments (0)