TypeScript for Beginners - Part 1

Welcome, everyone! Today we will take the first step in our journey to learn TypeScript. If you are a TypeScript beginner, you may be wondering. What is TypeScript? And how can it benefit you in your role as a developer 🤔 Well, the high-level summary is, TypeScript is a powerful superset of JavaScript that includes all of the functionality of JavaScript, as well as additional features like static typing, improved code documentation, and error prevention to improve quality and maintainability of your code 🙌

Now you might be thinking, why should I learn TypeScript? I already know JavaScript. And that is a great question! TypeScript was created to help large teams scale their JavaScript projects, and it has become increasingly popular among developers. Imagine this scenario. The current feature you are working on is dependent on an SDK you are consuming from another internal team or external package, the parameters for the method you are calling are poorly defined, and there is no JSDoc documentation either. This means you have no idea what types the parameters should be, and now you have to waste time and go looking into code you may be unfamiliar with to see what you need to pass to that method to make it work. With TypeScript, however, you can set an expected type for each parameter in your methods (static typing). That way, if you pass a parameter with a type string that does not match the expected type number, you will immediately get an error and be able to fix it before the code is even run. That is just one small example of the many benefits of TypeScript and how it helps you work at scale.

In this part 1 of the Beginner TypeScript series, we will provide an overview of what TypeScript is, why it exists, and some of its basic features. Additionally, we will guide you through setting up your first TypeScript project, so you can start experimenting with the language and seeing its benefits for yourself. And do not worry if it does not immediately click for you. It did not for me either, but the more you use it, the more ah-ha moments you have, and the more used to it you will become. So, let us get started 🚀

What is TypeScript?

We know from the above that TypeScript is just a superset of JavaScript, but with more features. But instead of simply regurgitating all of that again, let me give you a visual example:

function whatIsYourName(name) {
	console.log(name);
}

whatIsYourName('Austin');

What do you mean that is not TypeScript? Of course, it is! Fundamentally TypeScript is just JavaScript with additional features. Browsers cannot interpret TypeScript directly, and instead, TypeScript just gets compiled to whatever version of JavaScript your project is targeting (we will talk more about that later.)

Why does it exist?

TypeScript originated from the shortcomings of JavaScript for the development of large-scale applications both at Microsoft and among their external customers. Challenges dealing with complex JavaScript code led to demand for custom tooling to ease the development of components across large organizations and teams. In other words, JavaScript was becoming exponentially more complex to scale as it continued to develop and mature as an ecosystem. Let us imagine this scenario. You are an engineer at Uber tasked with creating a function to validate the age of a user when they request a ride. Would the below code be sufficient to get the job done?

function requestRide(options) {
  if (options.user.age < MIN_AGE) {
    console.log(`Sorry, you must be at least ${MIN_AGE} years old to request a ride.`);
    return false;
  }
  console.log(`Age verified. Enjoy your ride!`);
  return true;
}

const rideOptions = {
  pickupLocation: "123 Main St",
  destination: "456 Park Ave",
};

const isRideRequested = requestRide(rideOptions);
console.log(isRideRequested);

If you said yes, then you are correct, the function is valid and would validate the age of the user requesting the ride. If you said yes, BUT the call to requestRide will throw a TypeError because options.user is undefined, then great spot (if you did not spot that is also okay as that is what we are here to learn about). So how could TypeScript have prevented this from happening even before running the application? To answer that, let us look at what this snippet above would look like using types from TypeScript:

const MIN_AGE: number = 21;

function requestRide(options: { pickupLocation: string, destination: string, user: { name: string, age: number } }): boolean {
  const { user } = options;
  if (user.age < MIN_AGE) {
    console.log(`Sorry, you must be at least ${MIN_AGE} years old to request a ride.`);
    return false;
  }
  console.log(`Age verified. Enjoy your ride!`);
  return true;
}

const user = { name: "John Doe", age: 20 };
const rideOptions = {
  pickupLocation: "123 Main St",
  destination: "456 Park Ave",
  user: user,
};

const isRideRequested = requestRide(rideOptions);
console.log(isRideRequested); // false

Let us start from the top. We see the variable MIN_AGE now has a :number appended to it, which means that we are specifically saying that the type for MIN_AGE is number, and therefore it should only be able to hold numbers like 20, 0.21, and -21. Unlike some other languages, TypeScript does not differentiate between decimal and integer numbers. To initialize MIN_AGE as a number, we do MIN_AGE: number = 21. Before we move on to looking at the requestRide function parameter types, I want to look at another way we could have typed MIN_AGE that does not involve specifying the number type directly. What I am referring to is called Implicit Typing. Implicit Typing is the practice of not explicitly specifying the type of a variable when it is declared. Instead, the type of the variable is inferred from the value assigned to it. For example:

let x = 5; // TypeScript infers that x is of type number
let y = "Hello"; // TypeScript infers that y is of type string

In the above example, x is inferred to be of type number because it is assigned the value 5, and y is inferred to be of type string because it is assigned the value "Hello".

With that out of the way, let us look again at the parameters for requestRide. Now you will almost immediately notice that this options: { pickupLocation: string, destination: string, user: { name: string, age: number } } looks nothing like this options. Before I explain why they are different, I want to go over Primitive Types and Object Types. In TypeScript, Primitive Types are the basic data types that include boolean, number, string, symbol, null, undefined, and void. These types represent a single, immutable value. Object Types include classes, interfaces, functions, arrays, and pretty much anything else that is not a primitive type. They are complex data types that can have properties and methods and can be used to model more complex structures. The main difference between Primitive Types and Object Types is that Primitive Types are immutable, while Object Types are not. Additionally, Primitive Types are passed by value, while Object Types are passed by reference. This means that when a Primitive Type is assigned to a variable, a copy of that value is created, and the variable refers to that copy. When an Object Type is assigned to a variable, the variable refers to the original object, not a copy of it.

If that did not make much sense, another way to put it would be to say Primitive types are like the basic colors you use to paint a picture. They include colors like red, blue, and yellow. Object types are like a paint set, they are more complex and can be used to make more complex pictures. They include things like paintbrushes, paint tubes, and palettes. The difference between them is that you cannot change the basic colors once you have them, but you can use the paint set to make many different pictures. Below you will also find a table with examples of each type:

PrimativeObject
numberfunctions
stringclasses
booleanarrays
voidobjects
undefined
symbol
null

With that explained, let us look at the typing for the options parameter in our TypeScript example. Beginning from the top requestRide(options: { pickupLocation: string, destination: string, user: { name: string, age: number } }): boolean we can see that options falls under the Object Type because it is an object which makes sense because we are passing the rideOptions object as the parameter. Within the rideOptions object, we see that we have three properties pickupLocation, destination, and user. pickupLocation and destination are both string types, so we just append :string to them, and now they are typed pickupLocation: string, destination: string. But what about user? Well, since user is another object just like its parent rideOptions, it also falls under the Object Type grouping, and therefore we also need to type every one of its properties. In this case, name and age both of which are Primitive Types, and therefore we simply append their types respectively user: { name: string, age: number }. This can sometimes be hard to visualize when it is all crammed into the parameters like this (but do not worry, TypeScript has a solution for this we will look at shortly), so it can also be helpful to visualize it as if it was a JSON object:

options: { 
  pickupLocation: string, 
  destination: string, 
  user: {
    name: string, 
    age: number 
   } 
}

Okay, before we move on to how to clean this up, you may have noticed I have not talked about the boolean at the end of our method definition function requestRide(options: { pickupLocation: string, destination: string, user: { name: string, age: number } })**: boolean**. In addition to typing the parameters (or inputs of a function), you can also type the value returned (or output) of a function by essentially appending whatever type the value returned is to the end of the function. This can be either a Primitive or Object Type. In the case of our validation function, we should return either true or false if the user is old enough to request a ride. So our return type is boolean, so we append :boolean on the end of the function before the curly bracket {.

Now how do we clean this function's parameters up because this function requestRide(options: { pickupLocation: string, destination: string, user: { name: string, age: number } }): boolean is way too hard to read and if options is a widely used type we would probably want to be able to reuse it without typing it all out over and over. This is where Interfaces and Type Aliases come to the rescue.

In TypeScript, both Type Aliases and Interfaces are used to define custom types, but there are some key differences between them. A Type Alias is a way to give a new name to an existing type. It allows you to define a custom type using the keyword type followed by a name and the type it represents. For example, you can create a Type Alias Person for an object that has properties like "name" and "age" using the following syntax:

type Person = { name: string, age: number };

An Interface, on the other hand, is a way to define a contract for an object. It specifies the structure and the shape of the object, including the properties and methods that it should have. For example:

interface Person {
    name: string;
    age: number;
    address: string;
    getName(): string;
    getAge(): number;
}

An Interface defines what an object should look like, but it does not define the implementation of the object.

The TLDR is: Type Aliases are used to create a new name for an existing type, like Person in the first example, while interfaces define the shape of an object, specifying the properties and methods it should have, like in the second example. Type aliases can be used to create a new name for any type, while interfaces are used to define the structure of objects. It is also worth noting that type aliases cannot be extended or implemented like interfaces can be, interfaces can be extended, and implemented in a class, but type aliases cannot. With that said, let us now clean up our example.

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

interface RideOptions {
  pickupLocation: string;
  destination: string;
  user: User;
}

const MIN_AGE = 21;

function requestRide(options: RideOptions): boolean {
  const { user } = options;
  if (user.age < MIN_AGE) {
    console.log(`Sorry, you must be at least ${MIN_AGE} years old to request a ride.`);
    return false;
  }
  console.log(`Age verified. Enjoy your ride!`);
  return true;
}

const user: User = { name: "John Doe", age: 20 };
const rideOptions: RideOptions = {
  pickupLocation: "123 Main St",
  destination: "456 Park Ave",
  user: user,
};

const isRideRequested = requestRide(rideOptions);
console.log(isRideRequested); // false

How To Get Started

Now that we know enough about TypeScript to be dangerous. How do we set up a project to start playing with it?

  1. First things first, we need to install NodeJS and Node Package Manager (npm) (if you do not have it installed already). You can grab the Current or LTS version via this link. You can verify that node and npm have been installed correctly by opening a terminal or command prompt and running the following commands: node -v and npm -v. This should print the version number of node and npm respectively.
  2. Next, we need to create a new directory for the project. We can do this through our terminal or command prompt by using the mkrdir command and then navigate to it via cd directory-name.
  3. Once in the newly created project directory, we can initialize the project by running the following command in the terminal or command prompt: npm init. This will create a package.json file in your project directory, which stores information about your project and its dependencies.
  4. Next, we need to install TypeScript as a development dependency by running the following command: npm install --save-dev typescript.
  5. Then, we create a tsconfig.json file in your project directory by running the following command: npx tsc --init. This will create a basic tsconfig.json file that you can edit to configure the TypeScript compiler. You will see that there are many options in the tsconfig.json that are commented out, but the only ones we need to focus on are:
{
  "compilerOptions": {
    "target": "es2016" /* Set the JavaScript language version for emitted JavaScript */,
    "module": "commonjs" /* Specify what module code is generated. */,
    "esModuleInterop": true /* Emit additional JavaScript to ease support for importing */,
    "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
    "strict": true /* Enable all strict type-checking options. */,
    "skipLibCheck": true /* Skip type checking all .d.ts files. */
  }
}
  1. Next, create a new file called index.ts in your project directory. This will be the entry point for your TypeScript application. Then add some basic TypeScript code to index.ts. For example:
function greet(name: string) {
    console.log(`Hello, ${name}!`);
}

greet("John Doe");
  1. Then in the terminal or command prompt, run the following command to compile your TypeScript code to JavaScript: npx tsc. This will generate a new file called index.js in your project directory.
  2. We can then use NodeJS to run our application with the following command: node index.js. This should then print "Hello, John Doe!" in the terminal or command prompt. You can keep working on your code in index.ts and run the command npx tsc to compile it to javascript and test it using node.

With that, we now close our part one for the TypeScript for Beginners Series. Thanks for reading, and stay tuned for part two 👀