Easy jump into TypeScript.

Aleksandra Martyna
intive Developers
Published in
13 min readApr 6, 2023

1. Introduction.

TypeScript is a strongly-typed superset of JavaScript. Behind this vague term hides a vast amount of features added into JavaScript:

  • Strong typing: TypeScript adds type annotations to JavaScript, making it easier to catch errors during development and improving the maintainability of code.
  • Enhanced IDE support: TypeScript offers better autocompletion, navigation, and refactoring tools in popular IDEs like Visual Studio Code.
  • Improved scalability: TypeScript supports modules, interfaces, and classes, making it easier to organize and scale large codebases.

2. Installation.

We are going to install TypeScript and after that, we will jump right into code and explain piece by piece what is going on.

Make sure you have Node.js installed. I’m using VSCode as my IDE. I will be using TypeScript and TS interchangeably.

How to run TypeScript from scratch:

  1. Create a folder of any name that will hold our project.
  2. Navigate to your project directory and open terminal/cmd.
  3. Run the following:
npm init -y
npm install typescript --save-dev
npx tsc --init

In these three steps we have created the package.json file, installed TypeScript and created TypeScript configuration file tsconfig.json. Later on we will cover some of the options for that file, but for now let’s leave it with the default values.

Create index.html file in your project root:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Learn typescript</title>
</head>
<body>
<div id="app"></div>
<script src="./index.js"></script>
</body>
</html>

As you can see we included the script file, but instead of TypeScript file we included JavaScript file. Browsers do not understand TypeScript. TypeScript files are being compiled into JavaScript that means that TS has backward compatibility. It can be used with existing JavaScript code, so developers can gradually migrate their projects to TypeScript.

Create an index.ts file in your project root directory. We will build a small component that fetches user data. We will start from pure JS and gradually add TypeScript features into our code.

We will use JSONPlaceholder api to fetch the data, and modern async/await syntax to consume promise-based API. https://jsonplaceholder.typicode.com/users

In your index.ts file:

(async () => {
let users;
let url = "https://jsonplaceholder.typicode.com/users";

const data = await fetch(`${url}`);
users = await data.json();
})();

So, there is no TypeScript here or is there?

Yes, it’s there. With all the juice.

3. Fundamentals.

If you hover over the url variable you will see in the tooltip:

The syntax in the tooltip is how you specify types:

let url: string = "https://jsonplaceholder.typicode.com/users/";         
// L Thats type annotation.

We didn’t do it and still TypeScript knows that the url is of type string. The same goes for constant data, if hovered upon it will show that the type of data is Response which is a build in Fetch API interface represents the response to a request. This process is called type inference.

There are few situations when TypeScript compiler would infer the type:

  • Variables are initialized — once you hover over the users variable you will see that it holds the type of any, because it was not initialized. Type any is like a wild card. It literally means that the variable can be anything and you will not receive any errors.
// No error, TypeScript thinks it's fine:
let url: any = "https://jsonplaceholder.typicode.com/users/";
url = 59023;

// Error: Type 'number' is not assignable to type 'string'.
let url: string = "https://jsonplaceholder.typicode.com/users/";
url = 59023;
  • Default values are set for parameters.
function log(message = "Hi from logger" /* message is infered as string */) {
return message;
}
  • Function return types are determined.
function sayHello() {
return "Hi there";
}

const greet = sayHello(); // const greet: string

Back to the code.

Let’s refactor our code and add helper function that will return the correct url. We will also add type annotations. Personally, I think it is a good practice to add type annotations anywhere you can, even if we can relay on type inference. It increases readability.

// It's easier to read and understand logic of a code, 
// especially if someone else has written it
// Instead of:
const ids = getIds();
// do:
const ids: Array<number> = getIds();
// With code above I don't have to think much if ids is a random string,
// or there is more than one id, even if naming suggest that there might
// be more than one id.
// At first glance I can see that this is an array
// that holds ids which are numbers.

// There are two ways of describing an array type, ex.
// Array<number> or number[]

Our helper function should return default url if we won’t pass an id.

(async () => {
let users: any;
let url: string = getUrl();

const data: Response = await fetch(`${url}`);
users = await data.json();
})();

function getUrl(
id: string | null = null,
url = "https://jsonplaceholder.typicode.com/users/"
): string {
return id ? url + id : url;
}

Function getUrl also accepts as a first parameter id and what this syntax means id: string | null = null is that id can be either string or null and if none argument is passed to a function it will defaults to null.

This way of writing type annotations is called union type.

A union type describes a value that can be one of several types. We use the pipe | to separate each type. It literally means “or”, so: string | number | null is a value that is string or a number or null.

function getUrl(params): string {
return ...; // L :string is how you define return type
}

But what is the type of users in our fetch function? At first the users are type of any, but we can predict what the type of users will be. It will be an array of user and a user consists of id, email, name, phone, username, website, company and address.

Also the address consists of its own properties, so does company.

There are two ways to define the shape or structure of a value in our case user object:

  • interface
  • type

An interface is a way to define a contract that a value must conform to. It specifies a set of rules that an object must follow in order to be considered an instance of that interface.

An interface can define properties, methods, and index signatures, and can be extended or implemented by other interfaces or classes. It is defined using the interface keyword, followed by the name of the interface and the rules that the interface defines.

In our example we will write an interface for a user object.

// We omit company and address for now
interface User {
id: number;
name: string;
phone: string;
username: string;
website?: string;
}

As you noticed we did put a ? behind a website. That means that this filed is optional and that resolves to:

whereas all the other properties must be defined. It kind of resembles to inheritance in object-oriented programming. Interfaces can be extended as well as classes can be.

Imagine that a user has some kind of permissions granted. We will also use an interface to describe the data of permissions.

interface Permissions {
viewPage: boolean;
createPage: boolean;
deletePage: boolean;
addUser: boolean;
}

interface User {
id: number;
name: string;
phone: string;
username: string;
website?: string;
}

I don’t have to type all those fields into a User interface, all I have to do is extend my User:

interface Permissions {
viewPage: boolean;
createPage: boolean;
deletePage: boolean;
addUser: boolean;
}

interface User extends Permissions {
id: number;
name: string;
phone: string;
username: string;
website?: string;
}

let user: User;

// This way user has access to all the fields
// that are both in Permissions and User Interface.

A type is a way to create an alias for a specific shape or structure of a value. It is defined using the type keyword, and can be used to create a new type from existing types, as well as to define new types from scratch.


// Type can be very simple:
type Text = string;
const description: Text = "It's a description";

// Type can be literal:
type City = 'Berlin';
const capitalOfGermany: City = 'Dubai';
// Would produce an error:
/* Type '"Dubai"' is not assignable to type '"Berlin"'. */

// Or it can be more complex:
type Address = {
city: 'Berlin' | 'Dubai' | 'London';
geo: {
lat: string;
lng: string;
};
street: string;
suite: string;
zipcode: string;
}

type Company = {
bs: string;
catchPhrase: string;
name: string?
}

// We have types for company and adress, let's put it into a User Interface

interface Permissions {
viewPage: boolean;
createPage: boolean;
deletePage: boolean;
addUser: boolean;
}

interface User extends Permissions {
id: number;
name: string;
phone: string;
username: string;
website?: string;
company: Company;
address: Address;
}

Lets add the correct types to the component:

type Address = {
city: 'Berlin' | 'Dubai' | 'London';
geo: {
lat: string;
lng: string;
};
street: string;
suite: string;
zipcode: string;
}

type Company = {
bs: string;
catchPhrase: string;
name: string?
}

interface Permissions {
viewPage: boolean;
createPage: boolean;
deletePage: boolean;
addUser: boolean;
}

interface User extends Permissions {
id: number;
name: string;
phone: string;
username: string;
website?: string;
company: Company;
address: Address;
}

(async () => {
let users: User[];
let url: string = getUrl();

const data: Response = await fetch(`${url}`);
users = await data.json();
})();

function getUrl(
id: string | null = null,
url = "https://jsonplaceholder.typicode.com/users/"
): string {
return id ? url + id : url;
}

Modules

In the above example we did put everything in one file.

“TypeScript provides modules and namespaces in order to prevent the default global scope of the code and also to organize and maintain a large code base.

Modules are a way to create a local scope in the file. So, all variables, classes, functions, etc. that are declared in a module are not accessible outside the module. A module can be created using the keyword export and a module can be used in another module using the keyword import.”

Good practice here would be to split. Let’s do that. I will create a new file called types.ts but you can name it anyway you want. I will put all the types and interfaces that we have created to that file like this:

// types.ts

interface Permissions {
viewPage: boolean;
createPage: boolean;
deletePage: boolean;
addUser: boolean;
}

export interface User extends Permissions {
id: number;
name: string;
phone: string;
username: string;
website?: string;
}

type Address = {
city: string;
geo: {
lat: string;
lng: string;
};
street: string;
suite: string;
zipcode: string;
};

The export keyword makes all the types private and visible only to that file. I did put an export only in front of User making it available to import anywhere in the application but with this one export I also made rest of the types and interfaces private and excluded them from global scope.

In order to use these types in index.ts I must import them like:

import { User } from "./types";

// As you can see there is no use for other types.
// They are included within User interface, hence I didn't export them.
// The extension .ts is not required.

(async () => {
let users: User[];
let url = getUrl();

const data: Response = await fetch(`${url}`);
users = await data.json();
})();

function getUrl(
id: string | null = null,
url = "https://jsonplaceholder.typicode.com/users/"
): string {
return id ? url + id : url;
}

There are more ways of how to use module imports. You can read more about it here.

4. Compile Time.

Remember to save all the files that we have created and run in your terminal:

npx tsc

After compilation is done we should see the index.jsfile generated. Lets have a look what is on the inside.

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
(() => __awaiter(void 0, void 0, void 0, function* () {
let users;
let url = "https://jsonplaceholder.typicode.com/users/";
const data = yield fetch(`${url}`);
users = yield data.json();
}))();

That looks very bizarre isn’t it? 9 line of our original code transformed to 13 lines of something very weird.

Instead of having a lean Promise we ended up with a generator, also “use strict” was added. This is where tsconfig.json file comes to play.

{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true ,
"strict": true,
"skipLibCheck": true
}
}

The result of index.js is determined by the target option in tsconfig.json file. By default our code will be compiled to ES2016 where async/await functions where not supported yet. TypeScript will add a polyfill and that’s how we end up with generator. In order to emit async/await without transpilation, you need to set the target to ES2017 or later. Let’s change the target option to “es2017” and see what we end up with.

{
"compilerOptions": {
"target": "es2017",
...
}
}

Result:

"use strict";
(async () => {
let users;
let url = "https://jsonplaceholder.typicode.com/users/";
const data = await fetch(`${url}`);
users = await data.json();
})();

Strict mode is determined by the strict option set to true in tsconfig.json file.

Open index.hmtlin the browser and see in the network tab if the code that we wrote works.

5. TypeScript with frameworks and libraries.

The frameworks have adapted typescript very well. When creating applications, whether it is Angular or React or Vue or anything else, we can use the CLI and just start our project with Typescript without any additional configuration.

Angular

In Angular, TypeScript is used extensively to define component classes, services, interfaces, and other types. For example, when defining a component, TypeScript can be used to define the component’s properties, methods, and the type of data that the component will receive or emit. TypeScript can also be used to define interfaces for HTTP responses, making it easier to work with data received from an API.

Angular docs says that “Knowledge of TypeScript is helpful, but not required.”

This is kind of true, but not really. There is no option for having Angular application without TypeScript. Once installed you are in TypeScript.

export class UserComponent implements OnInit, OnDestroy {
users: User[] = [];
private subscription: Subscription;

constructor(private usersService: UserService) { }

ngOnInit(): void {
this.getUsers();
}

ngOnDestroy(): void {
this.subscription.unsubscribe();
}

private getUsers(): void {
this.subscription = this.usersService
.getHeroes()
.subscribe(users => this.users = users);
}
}

Code above is a simple Angular component that fetches user data using Angular service. The first thing you notice are type and return type annotations. Level of strictness of how your code should be maitained are set within TSLint which is also a part of Angular application. The second thing are access modifiers: public, private, protected — this is TypeScript. Public is the default and can be omitted (users). Private filed is accessible outside the class at runtime but it helps ensure we do not access that field improperly. In other words private field is not really private once an app is running.

JavaScript has its own access modifiers and they can definitely ensure that a filed won’t be accessible anywhere.

export class Users {
#users = [] // this is a private file
}

Truth to be told, if you are starting with Angular the basic knowledge of TS is enough. After reading this article you should be ready and set to go to start working with TypeScript.

React

React is a JavaScript library that allows developers to build reusable UI components that can be rendered on the web. Developers can start a new React project with TypeScript by creating a new project using tools such as Create React App. This generates a new React project with TypeScript support.

Let’s have a look on how would a component that renders user data can look using TypeScript.

import { UserCard } from "./User";

interface UsersProps {
title: string;
users: ComponentProps<typeof UserCard>[];
}

export const Users = ({
users,
title = "Users list",
}: UsersProps) => {

return (
<>
<h1>{title}</h1>
<ul>
{users.map((user, index) => (
<UserCard key={index} userData={user} />
))}
</ul>
</>
);
};

In the code above we crated a component that renders a list of users. We destructurized props object and gave it a type annotation of UserProps.

UserProps interface consists of two properties: title which is a string and users which is an array of user. As I mentioned before, to correctly type an array we use syntax like:

Array<UserCard> 
// or
UserCard[]

So, what is going on here with this ComponentProps?

6. Utility types and extras.

users: ComponentProps<typeof UserCard>[]

ComponentProps is a type created by React team. In the example above we are nesting components. Users component renders a list of UserCards. UserCards takes all the properties of a single user in users array. A logic way to keep the types for UserCard would be within UserCard. Since we already imported UserCard we can also get all the types for props that UserCard receives.

The ComponentProps<typeof UserCard>[] literally means take the type of UserCard props and make it an array.

Type like this takes advantage of utility types.

TypeScript provides several utility types to facilitate common type transformations. These utilities are available globally.

Utility types comes handy when we don’t want to double the code that we write. Consider having a User interface and we also want a small avatar that would display only the username and website:

interface User {
id: number;
name: string;
phone: string;
username: string;
website?: string;
}

type Avatar = Pick<User, "username" | "website">;

Type Avatar literally resolves to: from User interface take only username and website where “and” is a union type.

7. Summary and next steps.

There is more and more stuff in TypeScript to make your code better and clearer. TypeScript reduces time to think about the code, although it forces you to write more code in the end. It’s a great warning system for JavaScript developers. I strongly encourage you to use TypeScript if you haven’t already.

Next steps, learn about:

--

--