Skip to main content
Complete Guide to Getting Started with TypeScript: Static Typing and Object-Oriented Programming | Kranio

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:


import { Product } from './product.model';

export const products: Product[] = [];

export const addProduct = (data:Product) => {
	products.push(data);
}


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:

import {subDays, format} from 'date-fns';

Examples of things we can do with this library:


const birthday = new Date(1991, 4, 6);
const rta = subDays(birthday, 30);
const str = format(rta, 'yyyy'/MM/dd);


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:


const data = [
	{
		username: 'GastĂłn Fuentes',
		role: 'Admin'
	},
	{
		username: 'Andres Mazuela',
		role: 'seller'
	},
	{
		username: 'Paola Mazuela',
		role: 'seller'
	},
	{
		username: 'Andrew Vasquez',
		role: 'customer'
	}
];


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:


export interface ProductService {
	createProduct(createDTO: CreateProductDTO): Product
	findProducts(): Product[]
	findProduct(id: Product[‘id’]): Product
	updateProduct(id: Product[‘id’], updateDTO: UpdateProductDTO): Product
}



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:


export interface CreateProductDTO 
extends Omit Product 'id' | 'createdAt' | 'updatedAt' | ' productCategory' {
  categoryId: string;
}


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:

export interface UpdateProductDTO extends Partial CreateProductDTO {}

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:


export abstract class Animal {
	protected name: string;

	constructor(name: string){
		this.name = name;
}

get move(): string {
	return ‘moving along!!’;
}

get greeting(): string {
return `Hello iÂŽm ${this.name}`;
}
}


‍

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 are solutions 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:


export interface ProductService {
	createProduct(createDTO: CreateProductDTO): Promise
}


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:


import { Category } from ‘./category.model’;

export interface Product{
	id: number;
	
	@Length(4, 140)
	@IsNotEmpty()
	title: string;

	@IsNotEmpty()
	price: number;

	@Length(4, 250)
	@IsOptional()
	description: string;
	
	category: Category;

	@IsUrl()
	@IsNotEmpty()
	Image: string[];
	
	categoryId: number;
}



The next step will be to define the DTO:


import { Category } from ‘../models/category.model’;
import { Product } from ‘../models/product.model’;

export interface CreateProductDTO extends Omit Product, ‘id’ | ‘category’ {
	categoryId: Category[‘id’];
}

export interface UpdateProductDTO extends Partial CreateProductDTO {}


After having our models and DTO, the next will be the interface where we establish the skeleton of the CRUD.


import { CreateProductDTO, UpdateProductDTO } from ‘../dtos/product.dto’;
import { Product } from ‘./product.model’;

export interface ProductService {
	createProduct(createDTO: CreateProductDTO): Promise;
	findProducts(): Promise;
	findProduct(id: Product[‘id’]): Promise;
	updateProduct(id: Product[‘id’], updateDTO: UpdateProductDTO): Promise;
}


After having the interface with the skeleton of how our CRUD would be, it's time to make the service where we will implement this interface.


import axios from ‘axios’;
import { CreateProductDTO, UpdateProductDTO } from ‘../dtos/product.dto’;
import { ProductService } from ‘../models/product-service.model’;
import { Product } from ‘../models/product.model’;

export class ProductHttpService implements ProductService {
	constructor(private url: string) {}

	async createProduct(createDTO: CreateProductDTO) {
		const { data } = await axios.post(this.url, createDTO);
		return data;
	}

	async findProducts() {
		const { data } = await axios.get(this.url);
		return data;
	}

	async findProduct(id: Product[‘id’]) {
		const { data } = await axios.get(`${this.url}/${id}`);
		return data;
	}

	async updateProduct(id: Product[‘id’], updateDTO: UpdateProductDTO) {
		const { data } = await axios.put(`${this.url}/${id}`, updateDTO);
		return data;
	} 
}


Conclusion

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.

‍

‍

Previous Posts

Augmented Coding vs. Vibe Coding

Augmented Coding vs. Vibe Coding

AI generates functional code but does not guarantee security. Learn to use it wisely to build robust, scalable, and risk-free software.

Kraneating is also about protection: the process behind our ISO 27001 certification

Kraneating is also about protection: the process behind our ISO 27001 certification

At the end of 2025, Kranio achieved ISO 27001 certification after implementing its Information Security Management System (ISMS). This process was not merely a compliance exercise but a strategic decision to strengthen how we design, build, and operate digital systems. In this article, we share the process, the internal changes it entailed, and the impact it has for our clients: greater control, structured risk management, and a stronger foundation to confidently scale systems.