Complete Guide to Getting Started with TypeScript: Static Typing and Object-Oriented Programming | Kranio
Complete Guide to Getting Started with TypeScript: Static Typing and Object-Oriented Programming
Team Kranio 17 de enero de 2023
Compartir:
In this article we will see what TypeScript is, how we can use this superset so that JavaScript goes from a scripting language to a strongly typed language, with objects and all the pillars of OOP.
đ Required knowledge: Basic JavaScript
What is TypeScript?
TypeScript is a superset for JavaScript, it gives JS strong typing, interfaces, classes, inheritance, and everything that languages like Java, C#, etc. have.
Initial setup
The first thing to do is to position ourselves in the folder where we will be working from the console and install TS, for this we will use this command in the console:
npm i typescript --save-dev
Then in the working folder, we will create a .gitignore file. This is used to ignore certain files that we do not want to go to our repository on GitHub. For this, there is the page:
Here we will put the types of files we want to ignore, these must be separated by commas. Generally, different operating systems and Node are ignored.
Then you create the .editorconfig file:
#Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false>
The next step is to create the src folder. This is where the .ts files (TypeScript files) will go.
Then we go to the tsconfig.json file, where most lines are commented out, what will be uncommented is outDir: "./dist",
this way it is configured that anything compiled will go to the dist folder. Another is rootDir: "./src",
that is, our main directory where all our TypeScript files will be is src and then when saving, automatic
transpilation to dist will be done.
Now for this to really work automatically, in the console of our folder we put npx tsc --watch and with this the transpilation is done automatically.
Then to run a file we will put the following in the console:
node dist/file.js
Typing
There is what is called type inference:
let xNumero = 1; --> this will be taken as a number
let xString = 'xCadena' --> this will be taken as a String
This is what we normally do in JS and we can also do it in TS.
â
But to have a function with parameters that come with explicit typing and an explicit return (this can only be done in TS)
const calcTotal = (prices: number[]): number => {
let total = 0;
prices.forEach((item)=>{
total += item;
});
return total;
}
This function receives a parameter prices which is explicitly typed as an array of numbers and explicitly returns a number. The rest works like a normal JS function.
Arrays
In arrays, data types can also be inferred, as well as explicitly given. These arrays can be of one data type or several. For example:
const prices = [1,2,3,4];
This is an implicit array of numbers, this is also the same in JS.
const mixed = [1,2,'hello',true]
This is a mixed array, where the typing would be (number | string | boolean)[]. This inference can also be seen in JS. Then we have an explicit mixed array:
const mixed: (number | string | boolean)[]
This is already specific to TS where first must go number, then string and finally boolean.
â
Union types
Union types are variables that can have several typings. For example, a variable that is of type string, or boolean, or number. That is, it has greater type flexibility. In JS when we declare a variable, if we do not give it a value, it will be of type any, it does not take a value until we assign it, and even that value could change. In TS this does not work the same (unless we assign the type any to a variable, but we would lose all the power of TS) in TS variables do have typing, in the case of union types it is like its own typing, which can be several. For example:
let userId: string | number;
This means that userId can be a string or a number and since this is a let, if in the future it changes value, this value can be a string or a number. But if we give it a boolean value, this will throw an error.
Custom data types
With union types we said they were like a custom typing. But note the "like a". Because union types and custom typing are not the same. With an example their differences will be clearer:
type XDatoPropio = string | number | boolean
As we see it is very similar to union types. But here it has the reserved word type. So, if we declare a variable, for example, estado (state). It would be like this:
const estado: XDatoPropio;
By doing this, estado could take as a value a string, a number or a boolean.
â
We also have literal data types. An example of this would be:
type Sizes = 'XS' | 'S' | 'M' | 'L' | 'XL';
That is, if I have the variable talla (size) and I give it the type Sizes, this variable can only have as a value a string that is XS, S, M, L, XL. Any other value would throw an error.
On the other hand, we have complex types, which act almost like an interface (but it is not the same) an example would be:
This is very interesting: the Product type has several variables and each with its own typing, we even have a quite particular variable like size, which has a ? sign meaning it is optional, also this size is of type Sizes. If we remember, this type can only have predetermined values. Therefore, if a variable has type Product, it is almost like an interface that receives certain values:
const products: Product[] = [];
This variable products is typed as an array of Product, so it will receive a title which is a string, a createdAt which is a date, a stock which is a number and if you want a size, which is a Sizes. But the last one is optional. But this goes further, we could have functions that give us everything a CRUD has, but it will be stored in memory and not in a DB.
Here we have an arrow function that serves to add a product to the products array. That is why it is called complex types.
Modular programming
Modular programming is a way of programming where we divide our files into folders. For example, one way to do this is to have a folder for the model, another for the service, and a file in the root that will be the main. In the model, we will have our own types and complex types, in service we will have the functions, and in main, we will execute those functions (this is one way to see modular programming, there are actually many. But its basis is to divide our files with a logic that supports this division)
For example, in our model folder, we have a file products.model.ts (it is not 100% necessary to put .model, but it is a way to reference that this is a model and a good practice)
export type Sizes = 'XS' | 'S' | 'M' | 'L' | 'XL';
export type Product = {
title: string,
createdAt: Date,
stock: number,
size?: Sizes
   };
The first thing you notice is the reserved word export. This is done because when programming modularly, our other folders must share files and for that they need to be exportable. In service, this would go:
Here we import Product, that is, we are exporting the complex type Product, which contains the type Sizes. We also have an array of products and the function that adds a product to that array.
Finally, the main file would be like this:
import { addProduct, products } from './product.service'
addProduct({
title: 'Pro1',
createdAt: new Date(1991,4,6),
stock: 12
});
What we are importing here is the service, which has the addProduct method and the products array. Finally, we execute the addProduct method providing the data it will have.
Libraries with TypeScript support
What is a library?
A library is a set of well-defined classes, interfaces, and functions ready to be used. When we talk about libraries with TS support, we mean libraries that can be used both in JS and TS.
An example of a library with TS support is date-fns:
First, we install this library by navigating to the root of our project in the console and running this command:
npm i date-fns âsave
To use it, we must import it in the file where we want to use it:
The first variable is a Date with year, month, day. The variable rta is subtracting 30 days from birthday. The variable str formats the date as year/month/day. Although we could format it as day/month/year if we wanted, or even in another way. In conclusion, what the date-fns library does is manipulate dates.
Libraries without TypeScript support
When we talk about libraries that do not have TS support, we mean libraries that do not have a typing system. So we will have to install it. An example of a library without TS support is lodash. The first thing we will do to use it is install it:
npm i lodash
Now we have to add typing to this library, for this we will install the following:
Then we import this library where we want to use it:
import _ from 'lodash';
The lodash library is used to simplify the handling and editing of objects, arrays, etc., since it provides many utility methods to do so. Examples:
Here we have an array that contains JSON objects. With lodash, for example, we could group these JSON objects by roles:
const rta = _.groupBy(data, (item)=>item.role);
Doing this would result in a JSON like the following:
{
admin: [ Â { username: 'GastĂłn Fuentnes', role: 'admin' } Â ]
seller: [
 {username: 'Andres Mazuela', role: 'seller'}
 {username: 'Paola Mazuela', role: 'seller'}
],
customer: [ Â { username: 'Andrew Vazques', role: 'customer' } Â ]
}
ENUMS
Enums are like a custom data type, but where each variable has its own option and it must be in uppercase. Example:
enum ROLES {
 ADMIN = "admin",
 SELLER = "seller",
 COSTUMER = "costumer",
}
The first thing you notice here, unlike a custom type, is the reserved word enum followed by the type ROLES, all in uppercase. Then there are the options, all in uppercase, with their respective value. When enums are used, you can only assign the value defined here, no other value can be given. Example:
role: ROLES;
The variable role will only take as value admin, seller, or costumer.
Interface
Interfaces resemble custom types but have substantial differences. An example of an interface is:
interface Product {
 productId: string | number;
 productTitle: string
 productCreateAt: Date;
 productStock: number;
 productSize?: Sizes;
}
The first thing you notice is that the reserved word interface is used, also this Product interface must be followed to the letter. Since an interface is like a contract, where everything stipulated must be fulfilled.
â
Another characteristic of interfaces is that they can be inherited. An example of this:
export interface BaseModel {
 readonly id: string | number;
 readonly createdAt: Date;
 readonly updatedAt: Date;
}
Here the reserved word readonly is used, which means this variable is read-only. So if we want to make changes to these variables, we cannot. This is a parent class, which has things that most interfaces have, such as an id, a creation date, and an update date. To inherit this we do the following.
export interface Product extends BaseModel {
 productTitle: string;
 productPrice: number;
 productImage: string;
 productSize: Sizes;
 productStock: number;
 productDescription?: string;
 productCategory?: Category;
 productIsNew?: boolean;
 productTags?: string[];
}
By using the word extends we inherit the BaseModel interface, with all its variables.
Interfaces can also have methods, for example, you can have an interface that contains a whole CRUD, but here the logic will not be included, only the structure of that method, with its parameters, and what it will return. When we implement an interface, remember that it must be followed to the letter, so if we create a method as an interface, we have to comply with its structure, but we provide the logic ourselves. An example of this:
This will be complicated to understand at first, mainly because of the DTOs, but it is something that will be seen later. In the method to create a product, we receive a creation DTO as a parameter and return a product, then we structure the other CRUD methods. To implement an interface, it is done like this:
Import { ProductService } from â../models/producto-service.modelâ;
export class ProductMemoryService implements ProductsService {}
The first thing is to import the interface, then it is implemented with the reserved word implements, here we also see class. But this is something that will be seen later.
DTO
DTOs or data transfer objects serve to facilitate communication between systems. For example, we have a DTO for creating and another for updating. For example, to create:
This DTO for creating a product inherits from an Omit (an Omit is a utility type) where we omit the variables id, creation date, update date, and product category. Otherwise, we receive all the other product variables plus a category id. This is the power of a utility type that allows us not to write much code in our DTO.
To create an update DTO, it would be done like this:
Here again we inherit a utility type, this time Partial. That is, all the variables of the create product DTO will be optional (since it is from the create product DTO, it includes the Omit). Again, we see the power of utility types, which save us from writing a lot of code, as well as reusing our creation DTO.
OOP
What is OOP?
OOP or object-oriented programming is a programming paradigm that uses objects and their interactions to design applications and computer programs. It is based on techniques including inheritance, abstraction, polymorphism, and encapsulation. Objects can be like those we see in everyday life, or even people, animals. We normally use them to represent models, mappings, etc., although these concepts may sound strange for now. An example of an object would be this:
Here there is an abstract class (generally abstract classes are used to be inherited) where we have the accessibility of the variable. Protected is used in abstract classes and these variables can only be accessed when inherited, another accessibility is private, in this case the variable can only be used inside the class and using this. Then there is public accessibility, which can be used inside the class or another if we import and instantiate it (these things will be seen later). By default, if we put nothing, the accessibility of a variable or method is public. Then we see the constructor, this must always be in a class and is where the class variables enter. For example, the name, we define a name as the one we have as a variable. Then we do this.name = name (this.name is the protected variable of the class and name is the variable defined in the constructor). Then there are get methods that return a string. A get method is a method that behaves like a class variable; for this to work it must be like get xMethodName and it cannot receive parameters, it can return something, or be void (which returns nothing).
With this, we have seen three concepts that make up object orientation. Encapsulation (which is the accessibility of variables and methods. That is, private, public, protected). Remember that protected is used in abstract classes and with that we are reaching another concept which is abstraction. Abstraction is a class that has variables and methods that many other classes use (not to be confused with generic classes) and with this we reach the third concept which is inheritance. Which basically is passing to a class the information of its âparentâ that has that information that the class will use and also other classes. But we still lack one more concept, which we will see now.
Polymorphism
Technically we have already seen polymorphism, since this refers to interfaces and interfaces, remember, are like a contract that has the skeleton either of a model, of a method (where we define its parameters and return) but the logic is given once it is implemented in some class. A real example of how an interface would be and how we would implement it in a class (I say ârealâ example because this is basically how it works, but summarized just to give the example)
export interface IDriver {
 database: string;
 password: string;
 port: number;
 connect(): void;
 disconnect(): void;
 isConnected(name: string): boolean;
}
This interface called IDriver, which is a very used interface for ORM (it doesn't matter if this is not understood, the important thing is to keep the concept) basically we have an interface that has as variables the name of a DB, a password, and a port, as well as a method to connect, which returns void, another to disconnect, which also returns void, and one to check if the DB is connected, which receives a name that is a string and returns a boolean. That is, if we implement this interface, we have to comply with everything it asks us. An example of a class that implements it is this:
export class PostgresDriver implements IDriver {
 constructor(
  public database: string,
  public password: string,
  public port: number
 ) {}
 connect(): void {}
 disconnect(): void {}
 isConnected(name: string): boolean {
  return true;
 }
}
This is a summarized way of how the Postgres ORM library works, where we implement IDriver. In the constructor, we have the interface variables, then we have the methods. This is how we implement everything the interface requires. This is called polymorphism. With this, we have covered the 4 pillars of OOP or object-oriented programming.
Singleton Design Pattern
What is a design pattern?
Design patterns in programming aresolutions to recurring design problems that are applied daily in the software industry. Design patterns allow developers to have a guide when establishing the structure of a program, making it more flexible and reusable.
What is Singleton?
Singleton is a design pattern that ensures a class can only be instantiated once and verifies that only one instance of the class is created. You may have heard the controversy about Singleton; it is even called an anti-pattern. This is because, in reality, you will very rarely use Singleton in your day-to-day programming, and the very definition of a design pattern is to solve everyday problems. Singleton is something you will use for very particular situations. I encourage you not to use it unless the situation requires it, but do have the knowledge since all knowledge adds up. Let's see an example of how a class would look using this pattern.
export class MyService{
 private name: string;
 static instance: MyService | null = null;
 private constructor(name: string){
  this.name = name;
 }
 static create(name: string){
  if(MyService.instance === null){
   MyService.instance = new MyService(name);
  }
  return MyService.instance;
 }
}
What we see here is that we have a static variable (this is another type of accessibility, that is, we are talking about encapsulation. Specifically, static means it can be used anywhere). This variable instance is typed as the MyService class itself or null (that is, it is a union type). Then there is a variable name which is a string. Something very particular in this class is a private constructor. Normally, by definition in object-oriented programming, constructors are public, but since we want it to be instantiated only once, it will be private, so you cannot do new xClass() (this is how instantiation is done, which will be explained in more depth later). There is also a static method, meaning accessible anywhere, which receives a string parameter and checks if the instance variable is null; if so, it instantiates the class (here we see how an instance is created) and finally returns the class with the instance variable.
Asynchrony and Promises
What is asynchrony?
JavaScript is a language that can work both synchronously and asynchronously. Why do I mention JS if this is a TS article? Well, because asynchrony occurs both in JS and TS. I think this concept can be better understood in JS. When does JS behave synchronously? JS behaves synchronously when all programming depends on us, so it simply follows the thread of how the function will execute. In the case of asynchrony, it does not entirely depend on us; an example of this is calling an API. The call depends on us, but how long the API takes to respond depends on the API and the server we call. Therefore, this will always be asynchronous. Speaking of API calls, in TS this changes compared to JS. In JS, we used fetch to call APIs. In TS, we will install a library called axios (I think we can still use fetch, but generally axios is used).
npm i axios
After installing axios, to use an asynchronous context, it is done as follows:
(async () => {
 function delay(time: number) {
  const promise = new Promise((resolve) => {
   setTimeout(() => {
    resolve(true);
   }, time);
  });
  return promise;
 }
})();
Here we simulate a delay for an asynchronous arrow function that takes a number as a parameter. A variable is created that is equal to an instance of a promise (we will see what this is later). This is "promising to return a boolean." It takes another parameter resolve; we use the setTimeout function, and it resolves as true after the time passed as a parameter. We return the promise variable. This is a way to simulate asynchrony with a promise.
What is a promise?
When there is asynchrony and we are, for example, calling an external API that will take some time to respond, programming uses a promise, which will be what we return at the end of the code block. A promise is a special JS object that links the producer code (the code that calls the API) with the consumer code (when we consume the API and wait for it to return something specific). An example of this:
Here again we see this interface. In this method, what we do is receive a creation DTO as a parameter and return a Promise object that in turn returns a product. By returning a product in a promise, we infer that this method is asynchronous and we will have to wait some time for it to respond.
How is axios used to consume APIs?
The first step, of course, is to install axios, which we have already seen. Then we would import axios.
import axios from âaxiosâ;
Then we must have a URL (the API's URL), which generally goes in the constructor. Then we use the reserved word await, use axios, followed by the HTTP verb we will use in the method, and as parameters, we pass the URL and the necessary information. An example:
export class ProductHttpService implements ProductService {
constructor(url: string) {}
async createProduct(CreateDTO: CreateProductDTO) {
  const { data } = await axios.post(this.url, CreateDTO);
  return data;
}
}
â
Note that this would not work, because in the constructor we set a variable url that is typed as string, normally we would put the API address we will consume as a string in the url. I did it this way just as an example. With the reserved word async we define that this will be an asynchronous function. We receive a creation DTO as a parameter and define a variable that is equal to the reserved word await (we use this whenever we use something async) we use axios, the corresponding verb, here since we are creating, it is post and as a parameter we send the url and the creation DTO.
Generic classes
When we saw interfaces we said not to confuse them with generic classes, well this is because generic classes are like the counterpart of an interface (no one defines it like that, but that's how I see it) while an interface is given typing to the parameter and a typing to the return. But the method has no logic and we give it when we implement it. Well in generic classes it is just the opposite, they are classes without typing, methods whose parameters have no typing and their returns neither. But this does have logic inside. Then when we use these generic classes we give them the respective typings. They have the flexibility to be any type, avoiding code duplication when we change typings. This is mostly used to create libraries, something we don't do much day to day. But it is also used for creating repositories and that is very common (by the way, we will not see repositories because that is beyond the scope of this course) basically keep the concept of a generic class, but don't use them much, unless you need to create a repository. It is a case similar to singleton, they are not things we will be using much. We can understand generics as a kind of "code template", through which we can apply a certain data type to several points of our code. They serve to reuse code, without having to duplicate it due to type changes and avoiding the need to use the "any" type. An example of this:
async update ID, DTO (id: ID, updateDTO: DTO) {
  const { data } = await axios.put(`${this.url}/${id}`, updateDTO);
  return data;
 }
As we see in this update method we receive an id and a DTO, the id will be of type ID and the DTO of type DTO. What kind of typing is this? In this method, being generic, we do not define it. But when we use this class, we will give it typing.
Decorators
In TypeScript, a Decorator is a structural design pattern that allows dynamically adding new behaviors to objects by placing them inside special objects that wrap them (_wrappers_). Using decorators you can wrap objects countless times, since the target objects and decorators follow the same interface. They serve to give some rules, so to speak, to variables. The first thing will be to install the decorators:
npm i class-validator âsave
An example of how to use decorators would be:
export class CreateCategoryDTO implements ICreateCategoryDTO {
 @Length(4, 140)
 @IsNotEmpty()
 name!: string;
 @IsUrl()
 @IsNotEmpty()
 image!: string;
 @IsEnum(AccesType)
 @IsOptional()
 acces?: AccesType | undefined;
}
The first decorator we see is Length which receives 4 and 140 as parameters. What this does is validate that our variable has a minimum of 4 characters and a maximum of 140. The next is IsNotEmpty. This verifies that it is not empty. Then IsUrl. This verifies that it matches the regular expression of a url. We also have IsEnum and we pass the enum AccesType as a parameter, this way it verifies that it is the enum we pass as a parameter. Finally, the decorator IsOptional. With this we verify that this data can be empty.
Putting everything learned together
We have now reached the final part of this article where we have seen TS from its definition, to OOP and other things. But what would the structure of a project using everything learned be like? I think you more or less have this answer after reading everything. Still, I will leave you how this structure would go:
The first thing would be the model where the structure of the database tables will go:
With this, we have concluded this article, where we saw everything about TS. Now you would be able to configure it, understand how typing works, arrays, union types, custom data types, ENUM, DTOs, interfaces, which is one of the fundamentals of OOP (polymorphism), common in frameworks like Nest where its core is TypeScript, asynchronism and promises. Something that is also fundamental when programming, whether in Node with any of its frameworks, even in the front-end.
Ready to take your development skills to the next level with TypeScript?
At Kranio, we have experts who will help you integrate TypeScript into your projects, improving the quality and maintainability of your code. Contact us and discover how we can boost your professional development.