Entre para a LISTA VIP da Black Friday

00

DIAS

00

HORAS

00

MIN

00

SEG

Clique para saber mais
Alura > Cursos de Programação > Cursos de Node.JS > Conteúdos de Node.JS > Primeiras aulas do curso TypeScript: desenvolvendo validações e tratando erros

TypeScript: desenvolvendo validações e tratando erros

Tipos utilitários - Apresentação

Olá! Boas-vindas! Neste curso, vamos nos aprofundar no TypeScript.

Meu nome é Emerson Laranja, sou instrutor de back-end.

Audiodescrição: Emerson se descreve como um homem negro de rosto arredondado. Ele tem barba e cabelos curtos e escuros. Usa óculos retangulares e veste uma camiseta lisa na cor azul. Ao fundo, uma parede branca iluminada em tons de verde e azul.

O que iremos aprender

Ao longo desse curso, você aprenderá alguns assuntos, como criar Objetos de Transferência de Dados (DTOs), utilizando o conceito de tipos utilitários do TypeScript. Também vamos validar os dados da nossa API através da Biblioteca Yup. Aprenderemos a criar erros personalizados e além de uma série de boas práticas.

O que iremos aprender:

Pré-requisitos

É recomendável que você já tenha algum conhecimento de JavaScript. Para isso, você pode acessar a formação Aprenda a programar em JavaScript com foco no back-end, na nossa plataforma. Lembramos que este é o segundo curso de uma sequência de cursos de TypeScript. Portanto, é essencial que você tenha concluído o curso TypeScript: construção de uma API com tipagem segura, que é o primeiro curso de TypeScript para back-end.

Vamos explorar tudo isso no nosso projeto Adopet, um sistema de gerenciamento de animais. Convidamos você a não apenas assistir aos vídeos do curso, mas também realizar os exercícios, tirar suas dúvidas no fórum do curso e interagir com outras pessoas na comunidade Alura do Discord.

Vamos começar?

Tipos utilitários - Criando DTOs com Omit e Pick no AdotanteController

Vamos dar continuidade ao nosso projeto do curso 1. Não se preocupe, vamos disponibilizar uma atividade com a configuração inicial do projeto. Além disso, adicionei ao projeto uma pasta chamada "docs", onde você encontrará os arquivos do Insomnia (software para fazer requisições) para o nosso projeto Adopet. Também disponibilizei o diagrama de entidade de relacionamento do Adopet.

Vamos começar pensando na segurança dos nossos dados no Adopet. Se acessarmos "src > controller > AdotanteController.ts", percebemos que, nesse arquivo, estamos recebendo todas as informações do req.body e retornando-as após criar um novo adotante.

Portanto, criamos um adotante, registramos no nosso banco de dados e retornamos essas mesmas informações. Isso pode ser um problema, já que estamos retornando também a senha, expondo a senha desse usuário. O ideal é conseguirmos criar o que se chama de DTO (Data Transfer Objects ou Objetos de Transferência de Dados), onde decidimos o que queremos receber e o que queremos retornar. Ou seja, vamos armazenar a senha no banco de dados, mas não vamos retornar isso para a pessoa usuária.

Criando tipos

Para isso, vamos usar o conceito de tipos. Vamos começar abrindo o nosso menu Explorer (Explorar), no lado esquerdo do VS Code, e selecionaremos a pasta "tipos". Em seguida, clicaremos no ícone de criar um novo arquivo, clicando no ícone do canto superior direito da coluna, e criaremos o arquivo tipoAdotantes.ts.

Nesse novo arquivo, criaremos o nosso primeiro tipo, que é o tipo da nossa requisição do nosso body, o que estamos recebendo do usuário. Vamos chamá-lo de TipoRequestBodyAdotante, então escreveremos, na primeira linha, type TipoRequestBodyAdotante = {}.

Poderíamos usar o próprio AdotanteEntity. Se pesquisarmos na barra centro-superior por AdotanteEntity e acessarmos esse código, percebemos que a AdotanteEntity temos todos os campos do nosso adotante.

//código omitido

@Entity()
export default class AdotanteEntity {
  @PrimaryGeneratedColumn()
  id!: number;
  @Column()
  nome: string;
  @Column()
  senha: string;
  @Column()
  celular: string;
  @Column({ nullable: true })
  foto?: string;

//código omitido

Mas não queremos passar o id, então, voltaremos ao tiposAdotante.ts, onde criaremos um novo tipo que omite esse id. Para isso, utilizaremos um tipo do TypeScript chamado Omit<>. Entre os sinais de menor que e maior que, passamos a entidade como referência, que no caso é o nosso AdotanteEntity, e qual campo queremos omitir, que no caso é o id.

import AdotanteEntity from "../entities/AdotanteEntity";

type TipoRequestBodyAdotante = Omit<AdotanteEntity, "id">;

Nessa linha, criamos um tipo que vai ter todos os campos dentro do nosso AdotanteEntity, menos id, porque não precisamos receber isso do usuário. Em seguida, duplicaremos essa linha de código e mudaremos de "Request" para "Response", no nome da variável, fazendo referência ao nosso objeto de retorno.

import AdotanteEntity from "../entities/AdotanteEntity";

type TipoRequestBodyAdotante = Omit<AdotanteEntity, "id">;
type TipoResponseBodyAdotante = Omit<AdotanteEntity, "id">;

Na resposta, ao invés de omitirmos vários campos, queremos selecionar quais campos queremos retornar. Por exemplo, retornar apenas o id, o nome e o celular. Para isso, ao invés de usar o Omit<>, poderíamos usar o Pick<> (Escolha), com o qual selecionamos, passando os campos desejados, separando os valores com o pipe (|).

import AdotanteEntity from "../entities/AdotanteEntity";

type TipoRequestBodyAdotante = Omit<AdotanteEntity, "id">;
type TipoResponseBodyAdotante = Pick<AdotanteEntity, "id" | "nome" | "celular">;

Agora criamos um tipo que possui apenas id, nome e celular, tendo como referência nosso AdotanteEntity. Precisamos agora exportar esses dois tipos para aplicá-los ao nosso AdotanteController.

import AdotanteEntity from "../entities/AdotanteEntity";

type TipoRequestBodyAdotante = Omit<AdotanteEntity, "id">;
type TipoResponseBodyAdotante = Pick<AdotanteEntity, "id" | "nome" | "celular">;

export { TipoRequestBodyAdotante, TipoResponseBodyAdotante };

Aplicando os tipos que criamos

Voltando para o nosso AdotanteController.ts, iremos para a função criaAdotante(), por volta da linha 8. Se colocarmos o mouse sobre esse Request, com um R maiúsculo, surge uma mensagem informando que o Express nos fornece, como parâmetros, os Params, um ResBody e um ReqBody, que é justamente o corpo da nossa requisição.

Então, após o Request, adicionaremos um sinal de menor que e maior que passando um objeto vazio no primeiro e no segundo parâmetro, porque o terceiro parâmetro e o corpo da nossa requisição. No nosso caso, o TipoRequestBodyAdotante, então codamos req: Request<{},{},TipoRequestBodyAdotante>.

export default class AdotanteController {
    constructor(private repository: AdotanteRepository) {}
    async criaAdotante(
        req: Request<TipoRequestParamsAdotante, {}, TipoRequestBodyAdotante>,
        res: Response
    ) {
        //código omtido
    }
}

Para entender porque eu fiz isso, dentro das chaves da função criaAdotante(), vamos escrever req.. Agora temos acesso a todos os métodos do Express. Se escrevermos req.body, teremos acesso ao body apenas com os campos que definimos. Portanto, temos a vantagem de poder utilizar as duas coisas, então faremos o mesmo com o Response, onde o nosso primeiro parâmetro é o nosso ResBody, que é o nosso TipoResponseBodyAdotante.

export default class AdotanteController {
    constructor(private repository: AdotanteRepository) {}
    async criaAdotante(
        req: Request<TipoRequestParamsAdotante, {}, TipoRequestBodyAdotante>,
        res: Response<TipoResponseBodyAdotante>
    ) {
        //código omtido
    }
}

Implementando nossos tipos

Faremos uma pequena modificação no nosso tipo, porque precisamos retornar erro em alguns casos. Por exemplo, na linha 37 do AdotanteController, estamos retornando um erro em uma mensagem. Em outros casos, precisamos retornar uma informação ou um dado, como na linha 26, onde retornamos o novoAdotante.

Então, voltaremos ao tiposAdotante.ts. Na linha onde criamos o TipoResponseBodyAdotante, recortaremos toda a informação que passamos, selecionando-a e pressionando "Ctrl + X", e atribuiremos um objeto que possui um data como campo opcional, porque vamos querer retornar um dado ou um erro.

import AdotanteEntity from "../entities/AdotanteEntity";

type TipoRequestBodyAdotante = Omit<AdotanteEntity, "id">;
type TipoResponseBodyAdotante = {
    data?: Pick<AdotanteEntity, "id" | "nome" | "celular">;
}

export { TipoRequestBodyAdotante, TipoResponseBodyAdotante };

Voltando no nosso AdotanteController.ts, precisamos fazer uma modificação no retorno da função criaAdotante, aproximadamente na linha 27. Em .json(novoAdotante), ao selecionarmos o novoAdotante e substituirmos por chaves ({}), o VS Code nos auxilia, mostrando que ele espera um data.

Ao escrevermos data:{}, ele indica o que precisamos passar, no caso, id, nome, celular. Ele está mercado o id com um erro, porque não recebemos mais isso da pessoa usuária. Então, temos que passar o id que estamos criando para o nosso novo adotante, escrevendo id:novoAdotante.id.

//código omitido

async criaAdotante(
    req: Request<TipoRequestParamsAdotante, {}, TipoRequestBodyAdotante>,
    res: Response<TipoResponseBodyAdotante>
) {

//código omitido

    await this.repository.criaAdotante(novoAdotante);
    return res.status(201).json({ data: { id: novoAdotante.id, nome, celular } });
}

//código omitido

O servidor está funcionando e parece que está tudo certo através do VS Code, portanto, vamos testar no Insomnia.

Testando nosso tipo no Insomnia

Ao abrirmos o Insomnia, na coluna da esquerda acessaremos "Adotante > Cria Adotante". Na requisição, manteremos os dados de nome, senha e celular.

{
    "nome":"Emerson",
    "senha":"Exemplo1!",
    "celular":"27999999999",
}

Preview:

{
    "data": {
        "nome":"Emerson",
        "celular":"27999999999",
    }
}

Notamos que, quando enviamos a requisição, recebemos apenas as informações de nome e celular, então não expomos mais nossa senha para todo mundo. Desse modo, conseguimos utilizar tipos utilitários do TypeScript: o Omit, para omitir informações, e o Pick, para selecionar os campos que queremos retornar.

Criamos uma segurança dos dados que recebemos e os dados que retornamos. Na sequência, aprenderemos como replicar isso para todo o nosso CRUD. Até lá!

Tipos utilitários - Adicionando tipos na resposta

O que nós fizemos até o momento foi criar uma validação da nossa entrada e nossa saída na nossa função criaAdotante(). Porém, precisamos fazer isso para o restante do nosso CRUD, para quando formos atualizar, deletar e assim em diante.

Padronizando todas as etapas do CRUD

Para isso, dentro do nosso arquivo AdotanteController.ts, copiaremos as linhas de definição do req e do res, dentro dos parâmetros de criaAdotante(). Em seguida, faremos a substituição de parâmetros de todas as funções que têm req e res, ou seja, atualizaAdotante(), listaAdotantes() e deletaAdotante().

Vamos até uma das funções, selecionaremos o trecho req: Request, res: Response e pressionaremos "Ctrl + D" para fazer a seleção múltipla de todos os locais com esse trecho. Após selecionarmos os parâmetros em todas as funções, pressionaremos "Ctrl + V" para colar, substituindo todos os parâmetros.

Recebemos diversos erros após essa substituição, e a nossa missão agora é resolver cada um deles. Começando pela função atualizaAdotante(), se deixarmos o mouse por cima do id, recebemos a informação de que a propriedade id não existe no tipo Objeto Vazio ({}). Isso acontece porque, no nosso Request, como dissemos tem os params como primeiro parâmetro, e estamos passando um objeto vazio.

Podemos fazer como fizemos com o nosso body, que é criar um tipo para ele. Para isso, abriremos novamente o arquivo tiposAdotante.ts. Copiaremos a linha TypeRequestBodyAdotante = Omit<AdotanteEntity, "id">;, pressionaremos "Enter" após essa linha e colaremos o trecho copiado no espaço abaixo. Nessa segunda linha, mudaremos o Body para Params, e podemos adicionar mais linhas em branco para melhorar a visualização.

import AdotanteEntity from "../entities/AdotanteEntity";

type TipoRequestBodyAdotante = Omit<AdotanteEntity, "id">;

type TipoRequestParamsAdotante = Omit<AdotanteEntity, "id">;

type TipoResponseBodyAdotante = {
    data?: Pick<AdotanteEntity, "id" | "nome" | "celular">;
}

export { TipoRequestBodyAdotante, TipoResponseBodyAdotante };

Os parâmetros que estamos recebendo dentro de Req.Params são, basicamente o id. Para isso, atribuiremos a essa nova variável um objeto que recebe o id: string. Feito isso, podemos exportar o nosso TipoRequestParamsAdotante.

import AdotanteEntity from "../entities/AdotanteEntity";

type TipoRequestBodyAdotante = Omit<AdotanteEntity, "id">;

type TipoRequestParamsAdotante = {id: string };

type TipoResponseBodyAdotante = {
    data?: Pick<AdotanteEntity, "id" | "nome" | "celular">;
}

export { 
    TipoRequestBodyAdotante,
    TipoResponseBodyAdotante,
    TipoRequestParamsAdotante
};

Voltaremos ao arquivo AdotanteController.ts e precisaremos fazer uma modificação selecionando novamente todas as aparições de req e res que possuem esse primeiro objeto vazio, que é basicamente, todo o nosso código.

Portanto, selecionaremos a primeira aparição de req e res como parâmetro e pressionaremos "Ctrl + D" para selecionar as outras três. Feito isso, apagaremos o objeto vazio ({}) depois do req e substituiremos por TipoRequestParamsAdotante.

Arquivo AdotanteController.ts com o exemplo da função criaAdotante() após a alteração. Todas as demais funções com req e res terão os mesmos parâmetros.

  async criaAdotante(
    req: Request<TipoRequestParamsAdotante, {}, TipoRequestBodyAdotante>,
    res: Response<TipoResponseBodyAdotante>
  )

Adicionando uma resposta de erro

Agora o problema do nosso id foi resolvido, mas temos outro problema na função atualizaAdotante(). No caso de não ter sucesso, ela retorna uma mensagem (message), mas não temos esse tipo message dentro do nosso JSON.

Retornando ao tiposAdotante.ts, nosso objeto de retorno tem apenas um data, para retornar os nossos dados. No caso da atualizaAdotante(), queremos retornar um erro. Para isso, adicionaremos um novo campo. Então, ao final da linha onde declaramos o data, pressionaremos "Enter" e, abaixo, vamos declarar o error que também será opcional (?).

Poderíamos declarar o erro sendo do tipo any, porque não sabemos qual que é esse erro. Porém, ao usar o any perdemos a validação. Existe um outro tipo que é o unknown (desconhecido). Usamos esse tipo quando não conhecemos o erro, mas queremos de alguma forma validá-lo. Deixaremos uma atividade que mostra a diferença do unknown para o any, mas, por enquanto, deixaremos o erro como unknown.

import AdotanteEntity from "../entities/AdotanteEntity";

type TipoRequestBodyAdotante = Omit<AdotanteEntity, "id">;

type TipoRequestParamsAdotante = {id: string };

type TipoResponseBodyAdotante = {
    data?: Pick<AdotanteEntity, "id" | "nome" | "celular">;
    error?: unknown;
}

export { 
    TipoRequestBodyAdotante,
    TipoResponseBodyAdotante,
    TipoRequestParamsAdotante
};

Retornando ao nosso AdotanteController.ts, onde tínhamos o .json({message}), substituiremos o conteúdo do objeto por error : message. Ou seja, queremos essa mensagem que estamos recebendo como conteúdo do erro.


//código omitido

async atualizaAdotante(
    req: Request<TipoRequestParamsAdotante, {}, TipoRequestBodyAdotante>,
    res: Response<TipoResponseBodyAdotante>
) {
    const { id } = req.params;
    const { success, message } = await this.repository.atualizaAdotante(
        Number(id),
        req.body as AdotanteEntity
    );

    if (!success) {
        return res.status(404).json({ error: { error: message } });
    }

    return res.sendStatus(204);
}

//código omitido

Copiaremos a linha return res.status(404).json({ error: { error: message } });, porque esse código se repete na nossa função deletaAdotante() e na atualizaEnderecoAdotante(). Portanto, colaremos esse padrão em todos os pontos que tinha o .json({message}).

Retornando arrays

Agora estamos recebendo um erro na listaAdotantes, porque deveríamos retornar um array, mas no TipoResponseBodyAdotante, retornamos apenas uma aparição. Para resolvermos, selecionaremos todo o código do Pick<> e pressionaremos "Ctrl + C" para copiá-lo. Em seguida, pressionaremos "Enter" e, na linha abaixo, colaremos o código e escreveremos colchetes ([]), para informar que também queremos que seja possível retornar um array desse tipo de AdotanteEntity.

//código omitido

type TipoResponseBodyAdotante = {
  data?:
    | Pick<AdotanteEntity, "id" | "nome" | "celular">
    | Pick<AdotanteEntity, "id" | "nome" | "celular">[];

  error?: unknown;
};

//código omitido

Quando voltarmos para o AdotanteController.ts, o erro persiste, porque precisamos fazer alguns ajustes. O primeiro ajuste é porque estamos buscando essa lista com o nosso TypeORM, que traz todas as informações, e não queremos isso.

Como já entendemos, queremos retornar só o ID, o nome e o celular. Além disso, nosso .json() espera um campo que seja do tipo data ou error. Então, já podemos corrigir para .json({data}). Definiremos esse data como sendo um map da nossa nova listaDeAdotantes, onde obteremos o que recebemos da lista de adotantes e passaremos apenas ID, nome e celular.

Para isso, dentro da função listaAdotantes(), antes do return, criaremos uma nova linha com o código const data = listaDeAdotantes.map(adotante=>{}). Portanto, construiremos um map() onde, para cada adotante, retornaremos um objeto com id desse adotante, o nome do adotante e o celular do adotante. Esse data que retornamos com essa função.

async listaAdotantes(
    req: Request<TipoRequestParamsAdotante, {}, TipoRequestBodyAdotante>,
    res: Response<TipoResponseBodyAdotante>
) {
    const listaDeAdotantes = await this.repository.listaAdotantes();
    const data = listaDeAdotantes.map((adotante) => {
        return {
            id: adotante.id,
            nome: adotante.nome,
            celular: adotante.celular,
        };
    });
    return res.json({ data });
}

Corrigindo a conversão

Corrigimos esse erro, mas na função atualizaEnderecoAdotante() apresenta outro erro, porque não podemos converter esse TipoRequestParams, que é o nosso req.body, para um tipo EnderecoEntity. Queremos que o endereco em req.body.endereco, seja uma entidade de endereço (EnderecoEntity). E, ao abrirmos nosso terminal, percebemos que possuímos apenas mais um erro.

Retornando ao arquivo tiposAdotante.ts, nem todas as nossas rotas precisam do parâmetro, ou seja, do TipoRequestParamsAdotante, na hora de criar o adotante. Como observamos, não precisamos passar o id, então ele também precisa ser também opcional, ou seja, id?: string.

import AdotanteEntity from "../entities/AdotanteEntity";

type TipoRequestBodyAdotante = Omit<AdotanteEntity, "id">;

type TipoRequestParamsAdotante = {
  id?: string;
};

type TipoResponseBodyAdotante = {
  data?:
    | Pick<AdotanteEntity, "id" | "nome" | "celular">
    | Pick<AdotanteEntity, "id" | "nome" | "celular">[];

  error?: unknown;
};

export {
  TipoRequestBodyAdotante,
  TipoRequestParamsAdotante,
  TipoResponseBodyAdotante,
};

Salvando esse arquivo e abrindo o Terminal, notamos que o nosso servidor não tem mais nenhum problema, então podemos testar no Insomnia. Ao abrirmos o Insomnia, tentaremos listar os nossos adotantes. Para isso, na coluna da esquerda, acessaremos "Adotante > Lista Adotantes". Quando mandamos uma solicitação, clicando em "Send", recebemos um objeto contendo um tipo data, que é um array contendo objetos com apenas ID, nome e celular de todos os adotantes do nosso Adopet.

{
    "data": [
        { 
            "id": 1,
            "nome": "Lucas",
            "celular": "99991234"
        },
        {
            "id": 2,
            "nome": "Emerson",
            "celular": "99991234"
        },
        {
            "id": 3, 
            "nome": "Emerson", 
            "celular": "27999999999"
        }
    ]
}

Assim, conseguimos estender esse comportamento de proteger o que recebemos e o que retornamos, não apenas na hora de criar, mas para todo o nosso adotante. O que vamos fazer na sequência é replicar esse comportamento para o nosso pet, com algumas diferenças.

Sobre o curso TypeScript: desenvolvendo validações e tratando erros

O curso TypeScript: desenvolvendo validações e tratando erros possui 142 minutos de vídeos, em um total de 51 atividades. Gostou? Conheça nossos outros cursos de Node.JS em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Node.JS acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas