TypeScript is basically JavaScript with a type system, and in this post we’ll cover some of the most fundamental concepts of TypeScript.
JavaScript is undoubtedly one of the most popular programming languages of the decade, particularly among web and mobile application developers. Unfortunately, JavaScript was designed for prototypes and middle-sized applications, not large-scale and robust software applications. This was evident in its design system’s flaws and inadequacies, such as dynamic (loose) typing, implicit type coercion, etc.
TypeScript is a statically typed, open-source programming language introduced by Microsoft in October 2012. Fundamentally, it provides optional typing to JavaScript. The TypeScript programming language builds on top of JavaScript by adding optional static types, making it a typed superset of JavaScript.
TypeScript is JavaScript with a type system, and in this post we’ll cover some of the most fundamental concepts of TypeScript.
Before we can start developing apps with TypeScript, we need to have the following installed:
Node is a JavaScript runtime environment. It ships with a tool called npm, which is short for Node Package Manager, responsible for maintaining a list of the packages (external libraries). Run this command to check if you already have npm installed:
There are different ways of integrating TypeScript into your program. To keep things simple in this tutorial, we will install it using npm. Run this command in your terminal to check if you already have it installed:
If installed, you should see the version number printed; else, an error message is displayed.
If you don’t have it installed, run this command to install the TypeScript compiler globally on your computer:
Upon successful installation, run this command to confirm the installation:
We will use the Visual Studio Code editor for this tutorial since it is very popular among developers, has a very useful integrated terminal and has built-in support for TypeScript. You can also use any other IDE of your choice.
First, we need to create a workspace containing a file where we can write our TypeScript program. To do that, run the following command in your terminal:
Files ending with .ts
extension tell VS Code that it is a TypeScript file. Launch the VS Code app, then copy and paste the code below to the main.ts
file:
To open the VS Code integrated terminal, press CTRL (or CMD) + ` on your keyboard. The integrated terminal should open at the bottom of the VS Code app, automatically pointing to your current working directory.
Now run this command to compile our file:
This command uses the TypeScript compiler we installed globally to compile our TypeScript code to a regular JavaScript code. You will notice that the compiler created a new file in the same directory as your TypeScript file named main.js
.
To execute this file, use the node command followed by the file name:
You should see the message "Hello, TypeScript” printed on your console.
TypeScript allows us to explicitly describe the type of an identifier (variables, function parameters and object properties). Type annotation in TypeScript is done using the “: type” syntax after an identifier, where “type” can be any valid type (string, number, boolean, etc.). Once an identifier is annotated as a string, only string values can be assigned to that identifier. The TypeScript compiler will throw an error if we attempt to assign a value other than a string to that identifier.
The type of the identifier comes after your identifier (variable) name and is preceded by a colon.
Let’s take a look at how to add types to primitives.
In the above example, the variables id
, name
and isLoggedIn
are annotated as number, string and boolean. As a result, any attempt to assign a value not of the type mentioned above would result in a type
error. For example:
Essentially, TypeScript is giving us feedback directly in our code editor to let us know that we are attempting to assign a value of type string
to a variable annotated as number
, which isn’t allowed.
Objects in JavaScript can have as many properties as possible. In TypeScript, we type an object by type-annotating its individual properties.
As seen above, lines 1 to 5 define the type of the person
object. The type annotation ensures that we don’t accidentally supply fewer or more properties during the assignment and that each property receives the correct type of value.
Consider the following:
TypeScript is trying to tell us that isLoggedIn
is a required property as per annotation but is not included in the initialization.
Let’s look at another scenario with a different type of error.
This second scenario tries to add a property that was not specified on the person
type. TypeScript immediately flags an error saying that we are attempting to add a property, sex
, to the person
object.
Arrays in JavaScript and other programming languages are a way of storing related elements in a single variable. We can annotate the type of a variable, function parameter and a return value to indicate that it is supposed to be an array.
Any attempt to include a value that is not of the specified type immediately causes TypeScript to throw a type error.
Essentially, the error is saying, “Hey! A boolean value cannot be included in an array that only expects strings.”
TypeScript has another array variation that can hold a fixed length of values, ordered with specific types called Tuple. Tuples are fixed in their length and types and are exclusive to TypeScript; they don’t exist in JavaScript.
The person
array declared above is referred to as a tuple with two elements. The first element is a string, and the second is a number.
Let’s look at some TypeScript errors that can occur if we don’t provide the correct value types.
As we can see, it cares about not only the types in the length but also the order of those types.
Any attempt to add a third value to the person
tuple also flags an error.
What happens when we try to access a method or property not present in a certain type?
TypeScript is aware that person[1]
is of type number
, and the number
type has no property subString
.
Enum is short for enumerated types, and it does not exist in JavaScript; it is unique to TypeScript. It allows us to define a set of named constants that can be numeric or string values.
Numeric enums are number-based. They store string values as numbers.
TypeScript assigns numeric values starting at 0 to North, 1 to East and so on.
We can also manually assign values to our constants.
String enums are similar to numeric enums except that they map string constants to string values. They are more readable and more often used than numeric enums. We can convert the numeric CardinalDirection
enum above into
a string enum like this:
Enums can also contain members or properties with mixed string and number values.
These kinds of enum are highly discouraged. Unless you have a strong reason, it is advised to refrain from using heterogeneous enums.
Also, any attempt to reference a constant that isn’t part of the enum will result in an error.
TypeScript is smart enough to recognize that we have a typo and even help with suggestions.
A union type allows us to give a value multiple possible types using the single pipe (|
) character. When you compose a union type from primitives, the union is exclusive.
However, the union over object type is inclusive, meaning the object can match more than one member type in the union.
We can see from the above example that Student
can have a type of Person
or SchoolMember
, and TypeScript is OK with it.
Since the union type allows us to give multiple types to a value, type narrowing enables us to perform a type check before working with the value.
In the example above, we use typeof
to check what type price is before performing our operations, and TypeScript is smart enough to know what we are working with.
TypeScript allows us to combine multiple types using an ampersand (&
), which is particularly useful for object types.
In this example, we made a new type, Professor
, that is just a direct intersection of Person
and Lecturer
.
When you omit a property, say yearOfExperience
in type Lecturer
, TypeScript will throw a type error with the following message:
Property 'yearOfExperience' is missing in type '{ name: string; age: number; specialization: string; }' but required in type 'Lecturer'.
In TypeScript, types like any
, never
, null
and undefined
do not refer to any specific kind of data. Hence they are called special types.
The any
type is a way to opt in and out of type checking during the TypeScript compilation process allowing you to work with existing JavaScript codebases. A variable annotated to have the type any
can accept
values of all types. This type is not advisable as it tends to defeat the purpose of TypeScript.
The type any
can also be used as an argument or a return type.
The looseFunction
has its argument type set to type any
; hence any value of any type can be passed to it when it’s invoked.
As mentioned above, the type any
can accept values of any type, and it is not type-checked at compile time.
In certain circumstances, this sacrifice may be too expensive for your application. If we want a variable to take any value while still being type-checked at compile time, then the unknown
type is our best bet. It is the type-safe
counterpart of the type any
.
Here TypeScript is saying the variable type is unknown; hence it can’t tell if the method toLowerCase
exists on the variable. To get around this, we need to use a type guard to narrow down the type.
The void
type is commonly used as the return type for functions that don’t return anything. When used as the type of a variable, the void
type almost always renders the variable useless as only undefined
can be assigned to it. Let’s see the void
type in action with the following examples.
The void
type is used mostly when annotating the return types of functions to say the function shouldn’t return any value.
In the example above, if you hover over the function’s name, you’ll see that TypeScript inferred the type for the return value to be void
even without it being annotated. This is because, as we can see, the function
doesn’t return anything, but we can also make it clear that it doesn’t return anything by annotating it.
In TypeScript, null
and undefined
are values of type null
and undefined
, respectively, but they can also be subtypes of other types. Like the void
type, they are less useful
because they are only assignable to types any
, unknown
, and themselves.
The never
type denotes values that will never occur. It is only used when you are sure something will never occur. When used with variables, it signifies the variable will never be initialized or assigned a value.
In the example above, TypeScript essentially says that the variable of type never
type can never be initialized, or no value can ever be assigned to it.
When used as the return type of a function, it means the function should never return. The function is either going to be one that always throws an exception, so there’s no chance for anything to be returned or a function that continuously runs in some sort of loop.
If we try to return something from the function infiniteLoop
, TypeScript will complain because there’s not supposed to be anything ever returned from that function.
Until now, we have always explicitly specified the types of our variables. Another cool thing about TypeScript is that in some scenarios where the type of a variable is not stated, the TypeScript compiler can guess its type from certain values in your code smartly, and this is called type inference.
Let’s take some examples to see where TypeScript can infer the types of our variables:
1. When we declare and immediately initialize a variable
Here, TypeScript can infer the type of the counter
variable based on the value assigned to it on line 1. Hence we could still get all the IntelliSense support and type-checking feature for type number
on our
counter
variable.
On the other hand, uninitialized variables would have the type any
.
2. Setting default values for parameters
Like initializing a variable explained above, TypeScript can also infer the type of a parameter with a default value.
In the example above, even though we didn’t explicitly tell TypeScript the types of our parameters, it was able to figure it out from the default value assigned to them.
3. Determined return values
Because our function is just the sum of two number variables, TypeScript can infer that its return type is number
. This makes it evident since adding numbers yields a number.
In TypeScript, a type alias assigns a new name to a given type. It doesn’t necessarily create a new type—it just provides a reference for the type. It is often used for reference types as aliasing primitives aren’t
useful though it can be excellent for documentation. A type alias is created by preceding the name you want to give the type with the type
keyword.
Type aliases can be reused in our code and can even be exported to be used in other files or modules.
A noticeable thing with our customType
above is that the married
property has a trailing question mark. Question marks are used to mark a property as optional. Hence any object annotated to be of type customType
may or may not supply the married
property.
There are instances when you know more about a type than TypeScript does. One way to inform TypeScript of such type is using type assertion. Type assertion is a technique that tells TypeScript to treat a certain value as a certain type. It is purely effective at compile time and doesn’t affect the runtime behavior of our code.
Type assertion can be done in TypeScript in two ways: using the as
keyword and using the angle bracket (<>
) notation.
Using the as
keyword
Let’s assume we want to get a canvas
element in our document.
At this point, all TypeScript knows about the canvas
variable is some HTML element. It doesn’t know if it is an input
, a div
, a canvas
or a paragraph
element. But
when we write:
Here, TypeScript knows that the canvas
variable isn’t just an HTML element but a canvas
element.
Another example is:
Using the angle bracket <>
notation
The angle bracket notation version of type assertion works exactly like using the as
syntax, but when using TypeScript with JSX, only the as
keyword can be used for assertion.
So, let’s convert the example above to use the bracket notation:
On line 4, we convert myVariable
from unknown
type to string
type, which is why we could access the length
property without any error from TypeScript.
Defining and using functions is one of the basic tasks in any programming language, and it’s no different with TypeScript. Functions are a way to group a set of related code into a logical block that can be reused.
TypeScript fully supports the existing syntax for functions in JavaScript and adds type information as its additional feature. The type information provides documentation and helps reduce the likelihood of bugs as we can’t pass or return incorrect data (types) from a type-safe function.
A typed function has the following syntax:
TypeScript ensures that the add
function receives exactly two arguments and must return a value of type number
. TypeScript would throw an error in any attempt to introduce a new argument or return a type other
than number
.
It’s also important to add that this works with all types of functions (arrow functions, anonymous functions), not just named functions.
The parameters defined in a function can be marked as optional or given a default value, but not both. A question mark precedes the name of optional parameters. Also, optional parameters are defined first, followed by required parameters. A required parameter cannot follow an optional parameter.
We can assign typed default values to function parameters to be used when a value is not passed as an argument to the parameter by the function caller.
Functionalities can be built into classes and objects created from those classes. JavaScript, until ES2015, had no such thing as classes. As with other features, TypeScript allows us to add types to the members of a class.
A simple class declaration looks like this:
The studentName
and studentId
are properties, while getStudentDetails
is a method of the class. The constructor method gets called when an instance of the class is created. The constructor
works very much the same as regular functions. They can have type annotations and default values. Let’s instantiate an object from the class defined above:
The line of code above creates an object named myStudent
from the Student
class. The two arguments supplied are what gets passed to the id
and name
parameters defined on the constructor
function.
Let’s see what happens when we create an object with fewer or more arguments than the constructor expects.
There are instances where we want to prevent some class members from being accidentally modified after they’ve been initialized in the constructor function.
To do that with TypeScript, we can precede such members with the readonly
keyword. This is not a JavaScript keyword—it is a modifier we use in TypeScript to mark certain properties in classes, objects, etc. It is very
handy to prevent accidental modification.
TypeScript flags an error on lines 9 and 13, saying that you cannot assign to name
because it is a read-only property.
Interfaces are used to create reusable types that describe the shape of objects. They are very similar to type aliases, but a key difference between them is that interfaces can only be used with objects, not primitives or type literals. If an object implements an interface, it must define all variables and implement all methods declared on the interface.
In the example above, notice that, unlike when defining a type alias, when defining an interface, there is no equals sign (=
) after the name of the interface. If any of the properties or methods defined in the IStudent
interface were not declared or implemented in the student
object, then TypeScript would flag an error except if the property was marked optional.
If you know beforehand the structure or shape of an object, you may want to declare an interface for that and make your object implement that interface. By doing so, you prevent any form of error or omission.
In TypeScript, generics allow us to define interfaces, functions and classes that are flexible and can work with multiple types, avoiding code duplication where necessary. For instance, we need to write a function that can either take a string, number or boolean argument and return it to the caller. Ordinarily, we would need to write three different functions, one for each.
Obvious code duplication. What if we were to define the same function for other types like tuples, objects and arrays? We’d keep duplicating the function by creating separate functions with different names. That’s where generics come to the rescue. Let’s rewrite the functions above using a generic type.
This function accepts an argument, and it returns that argument. We customized the function to work with different types, so the input type of this function is what will be the return type as well.
You may have noticed the <T>
, known as the Type Parameter. Type parameters are placed inside an opening and closing angle bracket immediately following the function name. Although the type parameter
can be anything, the uppercase letter T
is conventionally used.
To call a generic function like our example above, we would write:
In the case of generic classes, the type parameter is placed immediately after the class name. A generic class may contain generic fields and generic methods.
For instance, we have a Student class with a name
field of type string
and an id
that can either be a string
or a number
.
From the example above, we can have more than one type parameter in our generics, and they only need to be comma separated in the angle bracket. Each type parameter is a placeholder for the actual type that would be passed when the class is instantiated.
As with classes and functions, you can also use generics with interfaces. Interfaces, as discussed earlier, only help define the shape of an object. The compiler only uses them for type-checking, strips them off afterward, and they never make it to runtime. For example:
While there is more to learn about TypeScript, this article exposes you to what you need to know to get started writing TypeScript. For more on TypeScript, you can check out the official documentation. To learn about using TypeScript with React, read this post.