What, Why, and How of Typescript for JavaScript Developers

Feature image

If you are a Javascript developer, you must have heard about Typescript at one point or another. If you have been reluctant about giving Typescript a try because you are not sure how it could serve you better than Javascript, you have come to the right place.

This guide gives an introductory but comprehensive guide to Typescript any Javascript developer would need to get started with it.

What is Typescript, what is its type system, and how would it benefit you as a Javascript developer to use Typescript in your next project? You will find answers to all these questions by the end of this article.

Note: I may be a bit biased towards Typescript. There’s no project that I start where I prefer JS over Typescript.


What is Typescript?

You can think of Typescript as a language that provides an additional layer over Javascript.

Why?

Although we initially write our code in Typescript, we can’t directly run Typescript on a browser like we run Javascript. Instead, Typescript goes through an additional compilation step to convert its code into browser-recognized Javascript.

So, even when we program in Typescript, the end-program that runs on the browser will be in Javascript.

Then, why do we use Typescript at all?

Though Typescript doesn’t provide additional functionalities than Javascript at runtime, it offers a set of features to ensure that we, the developers, can write less error-prone and better maintainable code compared to when using just Javascript.

How does Typescript do that?

Typescript, as the name suggests, introduces a type system on top of vanilla Javascript. Whereas with Javascript, the type of a variable is dynamically assigned, Typescript forces us to pre-define the type of the variable we are declaring.

With Javascript, we can assign an integer value to a variable in the first line and assign a string value to it in the next.

let jsVar = 0;
jsVar = "js";

But with Typescript, we can restrict this behavior by explicitly declaring a type for a variable. If we try to assign a string to a variable of type “number”, it generates an error.

let tsVar: number = 0;
tsVar = "ts"; //error
VS Code alerting of wrong type assignment

VS Code alerting of wrong type assignment

In a gist, this is what Typescript does differently than Javascript: use types to prevent us from making silly mistakes in our code.


How Typescript improves upon Javascript

While the lack of the ability to define types is not necessarily a deficit in Javascript, it gives too much freedom to programmers, which, inevitably, results in them writing bad code.

let aNumber = 123;

aNumber = {
    name: "John",
    age: 23
}

In the above scenario with Javascript, there’s nothing to stop the developer from using the aNumber variable to represent an object. While it isn’t an error that would crash the program, it beats the purpose of using variable names to self-document the code.

Typescript easily solves this issue by defining the type of the variable during declaration so that it can’t be assigned to a value of another type.

let aNumber: number = 123;

If another developer has access to this variable in your program, they can now rely upon its value being a number exactly as the name suggests.

function isEligible(personObj) {
    return personObj.age > 34;
}

let john = {
    name: "John",
    age: 23
};

isEligible(john);

In this case, the isEligible function expects an object that has a field named age. But Javascript doesn’t have a way to guarantee that the argument passed to the function will be, in fact, an object or it will have a field named age.

Again, Typescript has the solution to this problem.

interface Person {
    name: string;
    age: number;
}

function isEligible(personObj: Person) {
    return personObj.age;
}

let john = {
    name: "John",
    age: 23
};

isEligible(john);

Now, this code may not make sense to you at the moment. But note how it ensures that the type of the variable passed is of the type Person, which is defined at the beginning.

Using Typescript will take away hundreds of careless coding mistakes from your program and prevent you from having to pull your hair out every time you encounter the silliest of bugs. It will also make your code better self-documented and increase its maintainability.

If you have been frustrated with the insufficient code suggestions provided for Javascript in an IDE, then you have another reason to give Typescript a try. The presence of types gives Typescript the ability to show better code suggestions in an IDE.


Using Types with Typescript

Basic Types

Typescript has a number of basic types that are pre-defined. Number, string, boolean, and array are a few examples of them.

You can find the complete list of basic types in the Typescript documentation .

Here are a few examples:

const num: number = 0;
const firstName: string = 'Juan';
const isValid: boolean = true;
const obj: object = {
    id: 1,
    name: 'Juan',
};

// Two ways to define arrays
const names: string[] = ['Juan', 'Sean', 'Jane'];
const dogs: Array<string> = ['Rex', 'Woof', 'Puppy'];

// any type variables can be of any type
let newVar: any = 'Hello World';
newVar = 89;
newVar = false;

// You can define the type as a union of several types
let numOrBoolean: number | boolean = 12;
numOrBoolean = true;
numOrBoolean = "hey"; // error

Note how any type reverts Typescript to behave the same way as Javascript. Since our purpose of using Typescript is to give a better structure to our code, avoid using any type whenever possible.

Similarly, try to avoid using a union of types, but if it is unavoidable, limit the number of types allowed in the union as much as possible.

Declaring custom types

Remember how I used a type called Person in a previous code example? But Person is not a basic data type in Typescript. I created the Person type according to my requirements to use it as the type of parameter accepted by the given function.

We use interfaces to define the basic structure of a new type we are introducing to the application.

interface Person {
    name: string;
    age: number;
}

Now, if we create a new object of type Person, it should have the fields name and age within it. If not, Typescript throws an error.

VS Code alerting of missing properties in custom types

VS Code alerting of missing properties in custom types

You can also define optional fields inside an interface.

interface Address {
    houseNumber: number,
    street: string,
    city: string,
    country?: string
}

const address1: Address = {
    houseNumber: 134,
    street: "Down road",
    city: "Berlin",
    country: "Germany"
}

const address2: Address = {
    houseNumber: 2254,
    street: "Up road",
    city: "London",
}

You can then use a custom type as the type of a field when defining another type.

interface Person{
    name: string;
    age: number;
    address: Address;
}

Extending Interfaces

In Typescript, you can inherit the properties of another type by extending its interface.

Assume that your application needs two different types, Person and Employee. Since an employee is also a person, it makes sense to inherit the person type’s properties when creating the Employee interface. It prevents code repetition.

You can quickly achieve this by extending the Person interface.

interface Person {
    name: string;
    age: number;
}

interface Employee extends Person {
    jobName: string;
    salary: number;
}

const employeeJohn: Employee = {
    name: "John",
    age: 34,
    jobName: "Javascript developer",
    salary: 54000
}

Function parameter types and return types

Similar to variable types, you can define types for function parameters and return values. While the parameter type is declared next to the parameter name, return type is declared just before the curly braces.

interface Car {
    id: number;
    color: string;
    sold: boolean;
}

function getSoldCarCount(cars: Array<Car>) : number {
    return cars.reduce<number>((acc, car) => acc + car.sold ? 1 : 0, 0);
}

const car1: Car = {
    id: 23,
    color: "red",
    sold: false
}

const car2:Car = {
    id: 78,
    color: "black",
    sold: true
}

const car3: Car = {
    id: 12,
    color: "yellow",
    sold: true
}

const cars: Array<Car> = [car1, car2, car3]

let soldCarCount = getSoldCarCount(cars);

With the type of the parameter and return value defined, we can guarantee that you or anyone else using this function won’t accidentally pass an object that doesn’t have the characteristics of the Car type.

You can also guarantee that the field sold in any object passed won’t be undefined or null. And it eliminates a number of scenarios that could throw an error during the runtime. If you were using Javascript, you would have to write more code to prevent the possibility of such an error occurring during runtime.

Similar to variables, you can define the return and parameter types as a union of several types.

function buyCar(car : Car): Car | boolean {
    if (car.sold === true){
        return false;
    }
    return car;
}

When you declare the accepted parameter or return type, objects of types that extend the initial type’s interface are also accepted as the argument or the return value.

enum Manufacturer {
    Fiat,
    Porsche,
    Audi,
    BMW
}

interface ImportedCar extends Car {
    manufacturer: Manufacturer
}

const newImportedCar: ImportedCar = {
    id: 456,
    color: "black",
    sold: false,
    manufacturer: Manufacturer.Fiat
}

buyCar(newImportedCar);

Using generics

With Typescript, you can define generic variables just as easily as what we have convered so far. If you are defining a generic function, you can use it to process data that belongs to any of the built-in or custom types.

function getInfo<T>(input: T): T {
    return input;
}

const stringInfo = getInfo<string>("Hello World");
const numberInfo = getInfo<number>(3321);
const carInfo = getInfo<Car>(car1);

What if you use the any type instead of generics?

Of course, you can change the above function to accept any type of arguments using the any type.

function getInfo(input : any) {
    return input;
}

const stringInfo: string = getInfo("Hello World");
const numberInfo: number = getInfo(3321);
const carInfo: Car = getInfo(car1);

However, this method doesn’t preserve the type of data passed on to the function. Instead, it records every argument passed as belonging to any type. Besides you should avoid the use of any.

With generics, though, you can preserve what type of data is passed on to the function. If you want to change the function logic according to the type of the data passed, using generics is better than accepting data of any type.


Using type aliases

When a particular field you want to use in the application could belong to one of several types, you can define its type as a union of those separate types.

function accept(input: string): string | boolean | number {
    if (input.length > 13){
        return false;
    } else if (input.split("/").length < 3){
        return input.split("/").join(".");
    } else {
        return input.split("/").length;
    }
}

function reject(input: string): string | boolean | number {
    if (input.length > 13){
        return true;
    } else if (input.split("/").length < 3){
        return input.split("/").join(".");
    } else {
        return input.split("/").length;
    }
}

Instead of having to rewrite the union every time you, like above, you can define an alias for the union using the type keyword.

type resultType = string | boolean | number;

function accept(input : string) : resultType {
    ...
}

function reject(input : string) : resultType {
    ...
}

Now, you won’t have to use a long union of types. Also, if you want to make a change to the return type of the function in the future, you now have to change only one line of code.


Type conversion

When one type is defined by extending another’s interface, the generated relationship between the two gives us the permission to convert objects defined in one of them to another.

Take the Car and ImportedCar types I defined before. First, I’ll create an object of type ImportedCar and see how conversion works on it.

const newImportedCar: ImportedCar = {
    id: 456,
    color: "black",
    sold: false,
    manufacturer: "fiat"
}

const convertedCar = <Car>newImportedCar; //no error

//another syntax for the conversion
const anotherConvertedCar = newImportedCar as Car;

This code compiles without an error. It makes sense that this conversion works because the ImportedCar type already possesses all the fields defined in the Car type.

If we try to access the manufacturer field defined in the object before the conversion, it generates an error because the converted object is of Car type.

convertedCar.manufacturer;  //error

This conversion works the other way around too. We can convert a Car object to an ImportedCar object.

const newCar: Car = {
    id : 234,
    color: "green",
    sold: true,
}

const convertedImportedCar = <ImportedCar> newCar;

In this case, if you try to access the new field the Car object previously didn’t have, manufacturer, you will see that it returns undefined.

convertedImportedCar.manufacturer;   //undefined

Conclusion

I hope this post cleared any doubts you had about using Typescript for frontend development. Since most of the features in Typescript are already similar to Javascript, you would be able to master Typescript in no time as well. It would definitely pay off in your next project.

And the next thing you know, you’d become a Javascript developer who can’t live without Typescript, just like myself.

Thanks for reading!