Understanding TypeScript: A Deep Dive into Variable Typing

Understanding TypeScript: A Deep Dive into Variable Typing

·

6 min read

Table of contents

JavaScript(JS) is a dynamically typed language, which means in JS, the developers don’t mention the type of the variable at the time of declaration. JS is very flexible in this regard, a JS variable can take any form. E.g.

let a = "Hello World"
a = 2

While this flexibility can be convenient, it can also lead to runtime errors when applying a method to a variable that is only compatible with a different variable type. For example:

let a = "Hello World"
a = 2

a.split(" ")
ERROR!
/tmp/JUjT4d44Gd.js:6
a.split(" ")
  ^

TypeError: a.split is not a function
    at Object.<anonymous> (/tmp/JUjT4d44Gd.js:6:3)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:86:12)
    at node:internal/main/run_main_module:23:47

Encountering such errors in a large codebase can be challenging, and it's not ideal for production applications. To address this issue, we need more control over the type of our variables. And, TypeScript (TS) does just that. It gives us more control over the type of our variables.

TS is a statically typed language, where the type of all the variables is known before the runtime. TS is a superset of JS, where it gets almost all of the features of JS with the additional power of controlling the types of variables. It even lets us create our own custom types.

The browser is incapable of rendering the typescript files directly. So, the TS code is first compiled into the JS code, and then it is rendered into the browser.

The previous code will not get compiled in TS, in the first place. So, we can isolate the error, way before it is run.

Now, we know that the reason TS is special is because of its control over the variable types. So, it is only fair to learn all ( or at least most ) of the TS variable types. Let's do that then.

Primitive Types

The primitive types are the most basic variable types. All the other types are to be constructed using these. These are number, string, and boolean

Unlike other languages, TS doesn’t have different types of integers and float. The ‘number’ type is used to assign both of them. Eg

String and boolean types are the same as in JS

let a: number = 5;

let b: string = "Hello World";

let c: boolean = false;

Arrays

Arrays are the collection of a single primitive type. For eg . Arrays can be notated as declared by both of the following ways.

let arr1: number[] = [1, 3, 9, 4, 7];

let arr2: Array<number> = [1, 3, 9, 4, 7];

TS also allows you to create immutable arrays. These can be initialized as .

let a : ReadonlyArray<number> = [1, 3, 9, 4, 7];

any

‘any’ type can hold( as the name suggests) any value in it. You can use it whenever you are unsure about what type a variable would be. It can be used to infer any variable, function, and pretty much anything that is syntactically legal.

let num: any = 3;
num = "Hello World";

However, it is not a good practice to use ‘any’ type in your application as it would be the same as using JS. What is the benefit of using TS then?

You can also stop other developers from using the ‘any’ type in your application by setting the ‘noImplicitAny’ property to true in tsconfig file. It will throw compilation errors if someone uses the ‘any’ type in your application.

null or undefined

values used to signal absent or uninitialized values.

Type Annotations

When you declare a variable using const, var, or let, you can optionally add a type annotation to explicitly specify the type of the variable. We were already doing type annotation all this while.

let a: number = 7; //annotated variable

let b = 7; //un-annotated variable

Most of the time, you don't explicitly need to annotate a variable, TS will do that for you.

Types can also be annotated on function parameters as well as return types to specify them.

summation(a: number, b: number): number{
    return a + b;
}

summation(2, 5)  //This will run perfectly

summation(2, "cats") //this will throw error, as the function
                     // demands both arguments to be of 'number' type

Union Types

You can annotate a variable with a combination of two or more types for eg

let a: (string | number) = "Hello";

a = 5; //It will work
a = "Car" //It will work
a = false; //It will NOT work

Type Aliases

The union type we just used was anonymous, as in it didn’t have a name. But with TS, we can give them a name. Not just union types but we can give a name to any other types. We can do that with the ‘type’ keyword. Eg

type numOrString = (string | number);

let a: numOrString = "Hello"; //Now we have a custom type called
                               // numOrString, which can be reused.

We can create custom types with specific keys with type aliases.

type Car = {
    maxSpeed: number,
    mileage: number,
    company: string,
    model: string
}

// We can use this custom type to annotate variables

Interfaces

Besides Type Aliases, there is one more way (probably better) to create custom types using TS Interfaces. An interface looks like this.

interface Car {
    maxSpeed: number,
    userRatings: number,
    company: string,
    model: string
}

This looks very similar to type aliases.

We can access the properties of an interface Type as.

let benz: Car = {
    maxSpeed: 250,
    userRatings: 4.2,
    company: "mercedes",
    model: "Mercedes-Benz C-Class"
}

console.log(benz.maxSpeed)

We can make a property immutable by using the ‘readonly’ keyword. Now you can access the property but can’t change it.

interface Car {
    maxSpeed: number,
    userRatings: number,
    readonly company: string,
    model: string
}

console.log(benz.company); //This will work just fine
benz.company = "BMW" //This will throw error

We can also make a property optional by using the ‘?’ operator. Now we may or may not provide its value. However, when we try to access it, the compiler throws an error stating "property may be undefined". We can handle this error as.

interface Car {
    maxSpeed: number,
    userRatings: number,
    readonly company: string,
    model: string,
    fourWheeler?: boolean
}

if(benz.fourWheeler != undefined){ 
    console.log(benz.fourWheeler);   //this will work just fine
}

An interface can also be extended by some other interface to use its properties.

interface Vehicle {
    userRatings: number,
    company: string,
    model: string
}

interface Car extends Vehicle{
    maxSpeed: number
}

The interface 'Car' will have all the properties of the interface 'Vehicle' plus the 'maxSpeed' property.

Literal Types

Literal types can be used when we allow a variable to only have certain values. For eg

let position: ('left' | 'center' | 'right') = 'center';

position = 'left' //will work fine
position = 'top'  //will NOT work

‘as’ keyword

The ‘as’ keyword is used to typecast a type to some other type.

The catch over here is that you can only cast to a more specific or a less specific version of a type. For eg

let a1: string = '1';
let a2: string = 'Hello';

let b1: number= a1 as string;  // this will work just fine
let b2: number= a2 as string;  // this will NOT work

Conclusion

Learning TypeScript is a great first step before learning Frameworks like Angular. And this was a nice introduction to the Typescript language. We learned why would we want to prefer TypeScript over JavaScript. We learned about various TS types, primitives, and the combination of them such as Union types, Literal Types, Type Aliases, and Interfaces.

There is still some more important stuff in Typescript to learn like Generics, but we will look at them in future blogs.