Introdução ao TypeScript
Jorge Nascimento
Posted on July 27, 2023
TypeScript é uma linguagem de programação de código aberto desenvolvida pela Microsoft. Ela é uma extensão da linguagem JavaScript que adiciona recursos de tipagem estática ao JavaScript, tornando-se uma opção popular para desenvolvimento de aplicativos web e outros projetos.
- TypeScript
- Tipos Básicos em TypeScript
- Tipos Objetos em TypeScript
- Generics em TypeScript
- Class em TypeScript
- Decorator
Vantagens do TypeScript:
Verificação de tipos em tempo de compilação
: Uma das principais vantagens é a detecção de erros em tempo de compilação, o que ajuda a evitar erros comuns em JavaScript. Isso ajuda a melhorar a qualidade do código e facilita a manutenção.
Melhor suporte a IDE
: O TypeScript é altamente integrado com ferramentas de desenvolvimento, como o Visual Studio Code, fornecendo autocompletar, realce de sintaxe e informações sobre tipos para melhorar a produtividade do desenvolvedor.
Facilita a colaboração em equipes grandes
: Com a adição de tipos estáticos, o código se torna mais autoexplicativo, facilitando o entendimento e a colaboração entre membros da equipe em projetos de grande escala.
Ecossistema maduro
: O TypeScript é amplamente utilizado e possui um ecossistema de bibliotecas e frameworks crescente, oferecendo suporte a várias tecnologias populares.
Código mais robusto
: A tipagem estática ajuda a evitar certos erros comuns, tornando o código mais robusto e seguro.
Desvantagens do TypeScript:
Curva de aprendizado
: Para desenvolvedores que não têm experiência com tipagem estática ou que estão familiarizados apenas com JavaScript, a curva de aprendizado pode ser um obstáculo inicial.
Compilação adicional
: Como o TypeScript precisa ser compilado para JavaScript antes de ser executado no navegador ou no servidor, isso adiciona um passo extra ao processo de desenvolvimento.
Tamanho do arquivo
: A adição de informações de tipo pode aumentar o tamanho do arquivo, mas isso pode ser mitigado com a compressão e minificação adequadas.
Problemas de compatibilidade
: Embora o TypeScript seja compatível com JavaScript, em alguns casos, bibliotecas JavaScript podem não funcionar perfeitamente ou exigir esforços adicionais para serem usadas com TypeScript.
Em geral, o TypeScript é uma excelente escolha para projetos em que a segurança e a manutenção do código são importantes. Ele ajuda a evitar muitos erros comuns em JavaScript e proporciona uma experiência de desenvolvimento mais robusta e produtiva. No entanto, é importante considerar o contexto do projeto e a familiaridade da equipe com o TypeScript antes de decidir usá-lo.
Inferência de tipos
A inferência de tipos é um recurso poderoso do TypeScript que permite ao compilador deduzir automaticamente o tipo de uma variável ou expressão com base em seu valor e uso. Isso ajuda a evitar a necessidade de declarar explicitamente os tipos em muitos casos, tornando o código mais conciso e menos propenso a erros.
O TypeScript usa um sistema de tipos estático, o que significa que os tipos são verificados em tempo de compilação, antes de o código ser executado. Isso é diferente do JavaScript padrão, que é de tipagem dinâmica e verifica os tipos em tempo de execução.
Quando você declara uma variável sem especificar explicitamente seu tipo, o TypeScript tenta inferir o tipo com base no valor atribuído a ela.
Aqui está um exemplo:
let mensagem = "Olá, TypeScript!"; // O TypeScript infere que 'mensagem' é do tipo 'string'
Neste caso, o TypeScript deduz que a variável mensagem
deve ser uma string porque ela recebeu um valor de texto.
A inferência de tipos também pode ocorrer em funções. Quando você define uma função e não especifica os tipos de seus parâmetros ou retorno, o TypeScript tenta inferi-los com base nos argumentos passados e no que é retornado dentro da função.
Aqui está um exemplo:
function soma(a, b) {
return a + b;
}
let resultado = soma(5, 10); // TypeScript infere que 'resultado' é do tipo 'number'
Neste caso, o TypeScript deduz que a função soma recebe dois argumentos numéricos e retorna um valor numérico, portanto, a variável resultado será do tipo number.
A inferência de tipos funciona bem em muitos casos, mas pode haver situações em que o TypeScript não é capaz de inferir o tipo corretamente. Nesses casos, você pode optar por adicionar anotações de tipo explícitas para garantir a precisão dos tipos ou fornecer mais informações ao compilador.
No geral, a inferência de tipos é uma das características fundamentais do TypeScript que ajuda a fornecer os benefícios da verificação de tipos estática sem a necessidade de especificar manualmente os tipos em todos os lugares. Isso torna o código mais legível, mais seguro e mais fácil de manter.
Configurando o typescript
Para configurar o TypeScript e começar a utilizá-lo em seus projetos, siga os passos abaixo:
Passo 1: Instalação do TypeScript
Primeiro, você precisa ter o Node.js instalado no seu sistema. Se ainda não tiver, faça o download e instale-o a partir do site oficial (https://nodejs.org/).
Após a instalação do Node.js, abra um terminal ou prompt de comando e execute o seguinte comando para instalar o TypeScript globalmente no seu sistema:
npm install -g typescript
Passo 2: Inicialização de um projeto TypeScript
Crie uma pasta para o seu projeto e navegue até ela no terminal. Em seguida, utilize o comando tsc --init
para gerar um arquivo de configuração chamado tsconfig.json
. Esse arquivo contém as configurações do TypeScript para o seu projeto.
tsc --init
Passo 3: Configuração do arquivo tsconfig.json
O arquivo tsconfig.json
permite que você configure o TypeScript para atender às necessidades específicas do seu projeto. Você pode personalizar as opções de compilação, direcionar a pasta de saída dos arquivos gerados, entre outras configurações. Abra o arquivo tsconfig.json
com um editor de texto e faça as alterações desejadas.
Alguns exemplos de configurações comuns:
-
target
: Determina para qual versão do ECMAScript o TypeScript deve compilar. Por exemplo, "ES5", "ES6", etc. -
outDir
: Especifica a pasta de saída dos arquivos transpilados (.js). -
strict
: Habilita várias verificações rigorosas de tipos. -
include
: Define quais pastas devem ser incluídas na compilação.
Passo 4: Escrevendo código TypeScript
Agora você pode começar a escrever o código TypeScript nos arquivos com a extensão .ts
. O TypeScript é uma linguagem que adiciona tipagem estática ao JavaScript, portanto, você pode declarar tipos para as variáveis, funções e objetos.
Passo 5: Compilando o código TypeScript
Depois de escrever seu código TypeScript, você precisa compilá-lo para JavaScript. Para isso, basta executar o comando tsc
no terminal na pasta raiz do projeto (onde está localizado o arquivotsconfig.json
).
tsc
Agora, você encontrará os arquivos JavaScript gerados na pasta de saída configurada no arquivo tsconfig.json.
Esses são os passos básicos para configurar e usar o TypeScript em seus projetos. A partir daqui, você pode explorar recursos avançados, como módulos, decorators, etc., para aproveitar ao máximo o poder do TypeScript no desenvolvimento de aplicativos escaláveis e mais seguros.
Existem ferramentas que o auxiliaram no seu desenvolvimento como tais como:
- TS Node Dev - permite que você execute diretamente arquivos TypeScript sem a necessidade de prévia transpilação manual.
- Zod - bibliotecas de validação de esquema typescript-first.
- TS Jest - Um transformador Jest com suporte a sourcemap que permite usar Jest para testar projetos escritos em TypeScript.
- Typescript Eslint - permite que o ESLint analise e valide código TypeScript de maneira mais precisa.
Essas sao apenas alguns exemplos de ferramentas que o ajudaram no seu desenvolvimento, porem existem inúmeras outras que vamos conhecemos conforme avancemos.
Tipos Básicos em TypeScript
Em TypeScript, existem várias tipagens básicas que podem ser usadas para declarar os tipos de variáveis. Abaixo estão alguns exemplos dos tipos mais comuns:
number
: Representa números, tanto inteiros como de ponto flutuante.
Exemplo:
let age: number = 30;
let price: number = 9.99;
string
: Representa sequências de caracteres, texto.
let name: string = "John Doe";
let greeting: string = "Hello, TypeScript!";
boolean
: Representa valores verdadeiro (true) e falso (false).
let isReady: boolean = true;
let isPlaying: boolean = false;
array
: Representa uma coleção de elementos do mesmo tipo.
let numbers: number[] = [1, 2, 3, 4, 5];
let fruits: string[] = ["apple", "banana", "orange"];
tuple
: É um array com um número fixo de elementos, em que cada elemento pode ter um tipo diferente.
let person: [string, number] = ["Alice", 30];
enum
: Permite definir um conjunto de valores nomeados com um valor numérico associado.
enum Color {
Red,
Green,
Blue,
}
let favoriteColor: Color = Color.Blue;
console.log(favoriteColor); // Saída: 2
any
: Permite que uma variável aceite qualquer tipo de valor. É usado quando o tipo não é conhecido ou quando estamos migrando um código JavaScript existente para TypeScript.
let data: any = 42;
data = "hello";
data = [1, 2, 3];
void
: Representa a ausência de um tipo de valor. É comumente usado como o retorno de funções que não retornam nenhum valor.
function sayHello(): void {
console.log("Hello!");
}
sayHello(); // Saída: Hello!
null
e undefined
: Tipos que têm valores null e undefined, respectivamente.
let x: null = null;
let y: undefined = undefined;
never
: Representa o tipo de valores que nunca ocorrem, geralmente usado para funções que lançam exceções ou entram em um loop infinito.
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {
// Código que nunca termina
}
}
Tipos Objetos em TypeScript
Em TypeScript, existem basicamente 3 tipos para lidar com objetos:
Object
O tipo object é um tipo primitivo em TypeScript que representa qualquer valor não primitivo, ou seja, qualquer valor que não seja number, string, boolean, symbol, null ou undefined. Isso inclui objetos, arrays, funções e outros tipos de dados complexos.
function printObject(obj: object) {
console.log(obj);
}
const person = {
name: "John",
age: 30,
};
const colors = ["red", "green", "blue"];
printObject(person); // { name: 'John', age: 30 }
printObject(colors); // ['red', 'green', 'blue']
Type Alias
Um type alias é uma maneira de criar um nome alternativo para um tipo existente ou para definir um novo tipo com base em tipos existentes. Isso pode ser útil para tornar o código mais legível, modular e reutilizável.
type Point = {
x: number;
y: number;
};
function printPoint(point: Point) {
console.log(`x: ${point.x}, y: ${point.y}`);
}
const pointA: Point = { x: 10, y: 20 };
const pointB: Point = { x: 5, y: 12 };
printPoint(pointA); // x: 10, y: 20
printPoint(pointB); // x: 5, y: 12
Aqui estão variações de type alias que podem ser usados para criar tipos mais complexos.
Union Type Alias:
type MyBoolean = boolean | string;
const isActive: MyBoolean = true;
const status: MyBoolean = "active";
Intersection Type Alias:
type Person = {
name: string;
age: number;
};
type Employee = {
company: string;
jobTitle: string;
};
type EmployeeInfo = Person & Employee;
const john: EmployeeInfo = {
name: "John Doe",
age: 30,
company: "ABC Corp",
jobTitle: "Software Engineer",
};
Tuple Type Alias:
type Coordinate = [number, number];
const point: Coordinate = [10, 20];
Mapped Type Alias:
type Person = {
name: string;
age: number;
};
type PartialPerson = {
[P in keyof Person]?: Person[P];
};
const partialInfo: PartialPerson = {
name: "Alice",
};
Esses são apenas alguns exemplos dos diversos usos de type aliases em TypeScript. Eles podem ser extremamente úteis para tornar o código mais legível e fácil de manter.
Utility Types
Existem diversos tipos utilitários que podem ser usados para criar tipos mais complexos ou manipular tipos existentes de maneira mais conveniente. Abaixo estão alguns dos tipos utilitários mais comuns com exemplos de código:
Partial<T>
:
Cria um tipo com todas as propriedades de T tornadas opcionais.
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Partial<Todo>, newTitle: string) {
return { ...todo, title: newTitle };
}
const myTodo: Todo = {
title: "Learn TypeScript",
description: "Study TypeScript types",
};
const updatedTodo = updateTodo(myTodo, "Master TypeScript");
console.log(updatedTodo);
// Output: { title: "Master TypeScript", description: "Study TypeScript types" }
Required<T>
:
Cria um tipo com todas as propriedades de T tornadas obrigatórias.
interface Options {
name?: string;
age?: number;
}
function createUser(options: Required<Options>) {
return options;
}
const user = createUser({ name: "John", age: 30 });
console.log(user);
// Output: { name: "John", age: 30 }
Readonly<T>
:
Cria um tipo com todas as propriedades de T tornadas somente leitura (imutáveis).
interface Point {
x: number;
y: number;
}
const p: Readonly<Point> = { x: 10, y: 5 };
// p.x = 20; // Erro! Não é possível modificar propriedades de um tipo somente leitura.
Pick<T, K>
:
Cria um tipo contendo apenas as propriedades de T cujos nomes estão em K.
interface Person {
name: string;
age: number;
address: string;
}
type PersonNameAndAge = Pick<Person, "name" | "age">;
const personInfo: PersonNameAndAge = { name: "Alice", age: 25 };
console.log(personInfo);
// Output: { name: "Alice", age: 25 }
Omit<T, K>
:
Cria um tipo contendo todas as propriedades de T, exceto as propriedades cujos nomes estão em K.
interface Car {
make: string;
model: string;
year: number;
}
type NewCar = Omit<Car, "year">;
const myNewCar: NewCar = { make: "Toyota", model: "Camry" };
console.log(myNewCar);
// Output: { make: "Toyota", model: "Camry" }
Esses são apenas alguns dos tipos utilitários disponíveis no TypeScript. Eles são úteis para criar tipos mais precisos, compreensíveis e seguros em suas aplicações.
Interface
Uma interface é uma forma de definir a estrutura de um objeto em TypeScript. Ela descreve os tipos de propriedades e métodos que um objeto deve ter. As interfaces também podem ser usadas para definir tipos para funções.
interface Animal {
name: string;
age: number;
speak: (sound: string) => void;
}
const dog: Animal = {
name: "Buddy",
age: 5,
speak: (sound: string) => {
console.log(`${sound} ${sound}`);
},
};
const cat: Animal = {
name: "Whiskers",
age: 3,
speak: (sound: string) => {
console.log(`Meow!`);
},
};
dog.speak("Woof!"); // Woof! Woof!
cat.speak("Meow!"); // Meow!
Note que os três tipos têm usos diferentes, embora possam se sobrepor em alguns cenários. O uso de type alias e interface é especialmente útil para criar estruturas mais complexas e legíveis, enquanto o tipo object é mais amplo e inclui qualquer valor não primitivo.
Generics em TypeScript
Em TypeScript, os "Generics" são uma ferramenta poderosa que permitem criar componentes reutilizáveis e flexíveis, permitindo que funções, classes e interfaces trabalhem com vários tipos de dados de forma segura durante a compilação.
Os Generics são representados pelo símbolo "<>"
, seguido por um nome de tipo genérico (por convenção, geralmente utiliza-se a letra "T"
, mas qualquer nome pode ser utilizado). Vamos ver alguns exemplos para entender melhor como funcionam.
Função com Generics
Suponha que desejamos criar uma função que retorne o primeiro elemento de um array, independentemente do tipo de dado presente no array. Para isso, podemos usar Generics.
function getFirstElement<T>(arr: T[]): T | undefined {
return arr.length > 0 ? arr[0] : undefined;
}
// Uso da função com diferentes tipos de arrays
const numArray: number[] = [1, 2, 3];
const strArray: string[] = ["a", "b", "c"];
console.log(getFirstElement(numArray)); // Saída: 1
console.log(getFirstElement(strArray)); // Saída: "a"
Neste exemplo, a função getFirstElement
usa um tipo genérico T
, que é inferido com base no tipo do array passado como argumento. Dessa forma, podemos obter o primeiro elemento independentemente do tipo do array.
Classe com Generics
Vamos criar uma classe genérica para representar uma caixa (box) que pode conter qualquer tipo de item.
class Box<T> {
private items: T[] = [];
addItem(item: T) {
this.items.push(item);
}
getItems(): T[] {
return this.items;
}
}
// Uso da classe Box com diferentes tipos de dados
const numberBox = new Box<number>();
numberBox.addItem(1);
numberBox.addItem(2);
numberBox.addItem(3);
console.log(numberBox.getItems()); // Saída: [1, 2, 3]
const stringBox = new Box<string>();
stringBox.addItem("apple");
stringBox.addItem("banana");
stringBox.addItem("orange");
console.log(stringBox.getItems()); // Saída: ["apple", "banana", "orange"]
Neste exemplo, a classe Box
usa um tipo genérico T
, que é utilizado para especificar o tipo dos itens que podem ser armazenados na caixa. Quando criamos uma instância da classe Box
, podemos definir o tipo específico dos itens que queremos armazenar.
Interface com Generics
Vamos criar uma interface genérica que representa um par de valores.
interface Pair<T, U> {
first: T;
second: U;
}
// Uso da interface Pair com diferentes tipos de valores
const numberAndString: Pair<number, string> = { first: 42, second: "hello" };
const booleanAndArray: Pair<boolean, number[]> = {
first: true,
second: [1, 2, 3],
};
Neste exemplo, a interface Pair é genérica e possui dois parâmetros de tipo, T
e U
, que representam os tipos dos valores do primeiro e segundo elemento do par, respectivamente. Assim, podemos criar pares com diferentes tipos de valores.
Os Generics são uma ferramenta poderosa para criar código flexível e reutilizável, permitindo que suas funções e classes trabalhem com diversos tipos de dados sem perder a segurança de tipo que o TypeScript oferece.
Class em TypeScript
Em TypeScript, a programação orientada a objetos é suportada, e isso envolve o conceito de classes, modificadores de acesso (access modifiers) e herança (inheritance). Vamos explicar cada um deles e fornecer exemplos de código para ilustrar o seu uso:
Classe (Class):
Uma classe é um modelo ou uma estrutura que descreve um objeto, definindo suas propriedades e comportamentos. Em TypeScript, as classes podem ser usadas para criar objetos reutilizáveis e organizar a lógica do programa de maneira mais clara e modular.
Exemplo de código:
class Animal {
// Propriedades
name: string;
age: number;
// Construtor
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
// Método
makeSound() {
console.log("Animal is making a sound.");
}
}
// Criando uma instância da classe Animal
const animal1 = new Animal("Leo", 3);
console.log(animal1.name); // Saída: "Leo"
animal1.makeSound(); // Saída: "Animal is making a sound."
Modificadores de acesso (Access Modifiers):
Os modificadores de acesso em TypeScript permitem controlar o acesso a propriedades e métodos de uma classe a partir de outras partes do código. Existem três modificadores de acesso principais:
- public: A propriedade/método é acessível de qualquer lugar, é o padrão quando nenhum modificador é fornecido.
- private: A propriedade/método só pode ser acessado dentro da própria classe e não é visível fora dela.
- protected: A propriedade/método é acessível dentro da própria classe e também nas classes derivadas (subclasses).
Exemplo de código:
class Person {
private name: string;
protected age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public introduce() {
console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);
}
}
class Employee extends Person {
private salary: number;
constructor(name: string, age: number, salary: number) {
super(name, age);
this.salary = salary;
}
public showSalary() {
console.log(`My salary is $${this.salary}.`);
}
}
const john = new Person("John", 30);
// john.name; -> Erro: 'name' é privado e só pode ser acessado na classe 'Person'.
// john.age; -> Erro: 'age' é protegido e só pode ser acessado na classe 'Person' e suas subclasses.
john.introduce(); // Saída: "Hi, I'm John and I'm 30 years old."
const employee1 = new Employee("Alice", 25, 50000);
// employee1.age; -> Erro: 'age' é protegido e só pode ser acessado na classe 'Person' e suas subclasses.
employee1.introduce(); // Saída: "Hi, I'm Alice and I'm 25 years old."
employee1.showSalary(); // Saída: "My salary is $50000."
Herança (Inheritance):
A herança é um conceito fundamental da programação orientada a objetos, onde uma classe (subclasse) pode herdar propriedades e comportamentos de outra classe (superclasse). Em TypeScript, usamos a palavra-chave extends
para criar uma relação de herança entre classes.
Exemplo de código:
class Shape {
protected color: string;
constructor(color: string) {
this.color = color;
}
public getColor() {
return this.color;
}
public draw() {
console.log(`Drawing a shape with color ${this.color}.`);
}
}
class Circle extends Shape {
private radius: number;
constructor(color: string, radius: number) {
super(color);
this.radius = radius;
}
public getRadius() {
return this.radius;
}
public draw() {
console.log(
`Drawing a circle with color ${this.color} and radius ${this.radius}.`
);
}
}
const circle1 = new Circle("blue", 5);
console.log(circle1.getColor()); // Saída: "blue"
console.log(circle1.getRadius()); // Saída: 5
circle1.draw(); // Saída: "Drawing a circle with color blue and radius 5."
Neste exemplo, a classe Circle
herda da classe Shape
. A classe Circle também redefine o método draw
, o que é conhecido como sobrescrita (override) do método da classe pai (Shape
).
TypeScript também possui todos os recursos para lidar com POO, tais como polimorfismo, herança, interfaces e classes abstratas.
Decorators em Typescript
Em TypeScript os decoradores são uma funcionalidade poderosa que permitem adicionar funcionalidades extras a classes, métodos, propriedades e parâmetros de função. Eles são comumente usados em frameworks e bibliotecas para estender o comportamento de classes e funções de uma maneira declarativa.
Utilizando Decorators
Passo 1: Configuração
Para começar, é importante ter uma configuração TypeScript que permita o uso de decoradores. No seu arquivo tsconfig.json
, verifique se a opção "experimentalDecorators": true
está habilitada:
{
"compilerOptions": {
"target": "es6",
"experimentalDecorators": true
}
}
Passo 2: Criando um Decorator
Vamos começar criando um decorador simples que exibe uma mensagem antes e depois da execução de um método. Neste exemplo, criaremos um decorador chamado logMethod
.
// Definição do Decorador
function logMethod(target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Antes da execução de ${key}`);
const result = originalMethod.apply(this, args);
console.log(`Depois da execução de ${key}`);
return result;
};
return descriptor;
}
Passo 3: Aplicando o Decorator
Agora que temos nosso decorador, vamos aplicá-lo a um método em uma classe:
class MinhaClasse {
@logMethod
soma(a: number, b: number) {
return a + b;
}
}
const minhaInstancia = new MinhaClasse();
console.log(minhaInstancia.soma(2, 3));
Ao executar o código acima, você verá a seguinte saída no console:
Antes da execução de soma
Depois da execução de soma
5
Isso demonstra que o decorador logMethod
foi aplicado ao método soma
da classe MinhaClasse
e funcionou conforme o esperado.
Criando Decoradores com Argumentos
Decoradores também podem ter argumentos. Vamos criar um decorador que aceita um argumento para definir a mensagem de log.
function logMessage(message: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Mensagem: ${message}`);
const result = originalMethod.apply(this, args);
return result;
};
return descriptor;
};
}
Agora, podemos aplicar esse decorador à nossa classe da seguinte maneira:
class MinhaClasse {
@logMessage("Executando a operação de soma")
soma(a: number, b: number) {
return a + b;
}
}
const minhaInstancia = new MinhaClasse();
console.log(minhaInstancia.soma(2, 3));
Ao executar o código, a saída será:
Mensagem: Executando a operação de soma
5
Decorators são uma funcionalidade poderosa, mas é essencial usá-los de forma adequada. Eles podem tornar o código mais complexo e difícil de manter se usados de maneira inadequada.
Nesse artigo temos o básico necessário para começamos a construir nossas aplicações de forma mais robusta com essa incrível linguagem.
- Links de referências e saiba mais.
- - Mini-curso de TypeScript - Willian Justen
- - Documentacao TypeScript
- - Repositorio - Techs Roadmap
Este post tem como objetivo ajudar quem esta começando no aprendizado das tecnologias web, além de servir como incentivo no meus estudos e a criar outros posts pra fixação do aprendizado.
Me paga um café ? :) | pix: nascimento.dev.io@gmail.com
Me Sigam :)
Posted on July 27, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.