DEV Community

Cover image for Drops #03: Usando aliases para importação de módulos em TypeScript!
William Queiroz
William Queiroz

Posted on • Updated on

Drops #03: Usando aliases para importação de módulos em TypeScript!

Introdução

E ae dev, tudo bem com você?

Você já deve ter trabalhado com projetos onde as importações de arquivos e módulos eram (ou ficaram) cada vez mais aninhadas. Em um determinado momento você se perde a cada "ponto, ponto, barra, ponto, ponto, barra" (e ainda espera um pouquinho pra ver se o editor de texto ajuda a completar o caminho, pra onde você realmente quer ir (profundo, não?).

Seria muito mágico se houvesse uma maneira de alterar isso:

import { MyClass } from "../../../../deep/module";
Enter fullscreen mode Exit fullscreen mode

Pra isso:

import { MyClass } from "@/deep/module";
Enter fullscreen mode Exit fullscreen mode

Pois é, e TEM!

Bora pro post?

Ah! mas antes disso... Esse post faz parte de uma série de artigos "drops" que tenho aqui! Veja a lista:


Iniciando o projeto

Vamos começar criando um projeto e inicializando o nosso package.json:

mkdir ts-module-aliases && cd ts-module-aliases && yarn init -y
Enter fullscreen mode Exit fullscreen mode

Em seguida, vamos adicionar algumas dependências de desenvolvimento, sendo elas:

  • O TypeScript (duh!);
  • O ts-node-dev (que será responsável por rodar o nosso código em modo de desenvolvimento);
  • O Jest (precisaremos configurar algumas coisas no Jest para que ele interprete os caminhos absolutos que usaremos no nosso código);
  • O tsconfig-paths (esse carinha será responsável por viabilizar o uso dos aliases).
  • O Babel (ficará encarregado de realizar a construção do nosso projeto, interpretando os aliases e transpilando o código com os caminhos respectivos).

Execute o comando:

yarn add typescript@4.0.3 ts-node-dev@1.0.0 jest@26.6.0 ts-jest@26.4.1 @types/jest@26.0.15 tsconfig-paths@3.9.0 @babel/core@7.12.3 @babel/cli@7.12.1 @babel/node@7.12.1 @babel/preset-env@7.12.1 @babel/preset-typescript@7.12.1 babel-plugin-module-resolver@4.0.0 -D
Enter fullscreen mode Exit fullscreen mode

Note que aqui foram fixadas as versões de cada dependência. Apenas para que a minha configuração e a sua sejam feitas com as mesmas versões dos pacotes.

Depois de instalar as dependências vamos iniciar as configurações!

Configurando o TypeScript

Na raiz do projeto, crie um arquivo tsconfig.json com a seguinte configuração:

{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "lib": ["es6", "dom"],
    "allowJs": true,
    "rootDir": ".",
    "outDir": "./dist/lib",
    "declarationDir": "./dist/@types",
    "declaration": true,
    "removeComments": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*", "**/*.spec.ts"]
}
Enter fullscreen mode Exit fullscreen mode

Criando a base do projeto

Depois de configurar o Typescript, vamos criar alguns arquivos partindo da raiz do projeto, dentro da pasta src:

src/services/BarService.ts:

export class BarService {
  bar() {
    console.log(`Hi! I'm bar method from BarService :)`);
  }
}
Enter fullscreen mode Exit fullscreen mode

src/controllers/FooController.ts:

import { BarService } from "../services/BarService";

export class FooController {
  private readonly barService: BarService;

  constructor() {
    this.barService = new BarService();
  }

  foo() {
    this.barService.bar();
  }
}
Enter fullscreen mode Exit fullscreen mode

src/index.ts:

import { FooController } from "./controllers/FooController";

const fooController = new FooController();

fooController.foo();
Enter fullscreen mode Exit fullscreen mode

Para finalizar, vamos adicionar o script no package.json que executa o nosso código em modo de desenvolvimento:

{
  "scripts": {
    "dev": "ts-node-dev --no-notify src/index.ts"
  }
}
Enter fullscreen mode Exit fullscreen mode

Perceba que até aqui, ainda não temos um bom exemplo de arquivos SUPER aninhados. Será possível visualizar isso ao criarmos os nossos arquivos de testes!

Configurando o Jest

Na raiz do projeto, crie um arquivo jest.config.js com a seguinte configuração:

// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html

module.exports = {
  clearMocks: true,
  coverageDirectory: "__tests__/coverage",
  coverageProvider: "v8",
  preset: "ts-jest",
  testEnvironment: "node",
  testMatch: ["**/__tests__/**/*.spec.ts"],
};
Enter fullscreen mode Exit fullscreen mode

NOTA: os testes da nossa aplicação, ficarão na raiz do projeto, dentro da pasta __tests__.

Vamos então criar os nossos arquivos de testes. Partindo da raiz do projeto, dentro da pasta __tests__:

__tests__/unit/controllers/FooController.spec.ts:

import { FooController } from "../../../src/controllers/FooController";
import { BarService } from "../../../src/services/BarService";

describe("Unit: FooController", () => {
  let fooController: FooController;

  beforeEach(() => {
    fooController = new FooController();
  });

  describe("foo", () => {
    it("should call bar method from BarService", () => {
      const spy = jest.spyOn(BarService.prototype, "bar").mockImplementation();

      fooController.foo();

      expect(spy).toBeCalled();
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

__tests__/unit/services/BarService.spec.ts:

import { BarService } from "../../../src/services/BarService";

describe("Unit: BarService", () => {
  let barService: BarService;

  beforeEach(() => {
    barService = new BarService();
  });

  describe("foo", () => {
    it("should call console.log", () => {
      const spy = jest.spyOn(console, "log").mockImplementation();

      barService.bar();

      expect(spy).toBeCalledWith(`Hi! I'm bar method from BarService :)`);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Olha ai, os benditos "ponto, ponto, barra, ponto, ponto, barra"!

Configurando os aliases no projeto

Vamos adicionar a configuração abaixo no tsconfig.json:

{
  "compilerOptions": {
    // (...)
    "baseUrl": "./",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Esse mapeamento, fará com que todo @/* seja um alias para ./src/* (com o baseUrl sendo a raiz do nosso projeto).

Vamos agora, fazer com que o ts-node-dev seja capaz de entender os nossos aliases. Adicione no script de dev (no package.json), O trecho -r tsconfig-paths/register:

- "dev": "ts-node-dev --no-notify src/index.ts"
+ "dev": "ts-node-dev -r tsconfig-paths/register --no-notify src/index.ts"
Enter fullscreen mode Exit fullscreen mode

A partir daqui, podemos alterar as importações! Altere isso:

import { FooController } from "../../../src/controllers/FooController";
import { BarService } from "../../../src/services/BarService";
Enter fullscreen mode Exit fullscreen mode

Para isso:

import { FooController } from "@/controllers/FooController";
import { BarService } from "@/services/BarService";
Enter fullscreen mode Exit fullscreen mode

NOTA: Altere todas as importações para usarmos o alias ao invés dos ../ ou ./.

Ao executarmos o comando yarn dev, já podemos utilizar os aliases durante o desenvolvimento, porém, ao executarmos o yarn test, o Jest ainda não é capaz de entender os caminhos que estamos utilizando...

Vamos adicionar a propriedade moduleNameMapper no arquivo jest.config.js e fazer a seguinte configuração:

const { pathsToModuleNameMapper } = require("ts-jest/utils");
const { compilerOptions } = require("./tsconfig.json");

module.exports = {
  // (...)
  moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
    prefix: "<rootDir>",
  }),
};
Enter fullscreen mode Exit fullscreen mode

Com isso, já é possível utilizarmos os aliases nas nossas importações!

O problema

Até aqui, conseguimos configurar os aliases e utilizá-los tanto nos arquivos de testes quanto no source do projeto. Entretanto, precisamos ainda configurar o comando de construção do nosso projeto, somente assim ele estará pronto para publicarmos e utilizarmos no ambiente produtivo.

Vamos configurar o comando yarn build para construir o nosso projeto, e o comando yarn start para executar a o pacote construído.

Adicione os scripts no package.json.

{
  "scripts": {
    // (...)
    "build": "tsc",
    "start": "node dist/lib/src/index.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

A seguir, execute o comando:

yarn build && yarn start
Enter fullscreen mode Exit fullscreen mode

Você perceberá que o projeto não pode ser executado, por conta do seguinte erro:

❯ yarn start
yarn run v1.22.5
$ node dist/lib/src/index.js
internal/modules/cjs/loader.js:968
  throw err;
  ^

Error: Cannot find module '@/controllers/FooController'
Require stack:
Enter fullscreen mode Exit fullscreen mode

Isso ocorre por que o tsc não é capaz de entender esses aliases, inclusive, para nossa versão de produção, pouco importa se estamos usando aliases ou caminhos relativos, o que importa é que funcione!

Outro problema também, é que se notarmos os arquivos que foram construídos na nossa pasta dist vamos encontrar todos os nossos arquivos de testes! O que não faz sentido algum ir pro ambiente de produção, não é mesmo?

Precisamos então:

  • Fazer com que o comando de build gere um pacote funcional.
  • Fazer com que o comando de build empacote apenas o código que irá para produção (e remover os arquivos de testes de lá).

Vamos fazer tudo isso com a substituição do tsc pelo babel!

NOTA: O babel será responsável por alterar os nossos aliases, para os respectivos caminhos no projeto

Configurando o Babel

Como já adicionamos as dependências do Babel no inicio do artigo, vamos começar o arquivo babel.config.js na raiz do projeto, com a seguinte configuração:

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: {
          node: "current",
        },
      },
    ],
    "@babel/preset-typescript",
  ],
  plugins: [
    [
      "module-resolver",
      {
        root: ["."],
        alias: {
          "^@/(.+)": "./src/\\1",
        },
      },
    ],
  ],
  ignore: ["**/*.spec.ts"],
};
Enter fullscreen mode Exit fullscreen mode

Com isso o babel irá alterar todo ^@/(.+) para ./src/\\1, e.g.: @/services/BarService para ../services/BarService.

Vamos agora criar o arquivo tsconfig.build.json que irá herdar todas as configurações do nosso tsconfig.json e será usado apenas para criar os arquivos de declaração de tipos do projeto (dentro da pasta dist/@types). Isso é necessário, pois o Babel não fará esse trabalho. Adicione o seguinte no arquivo:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "rootDir": "./src"
  },
  "exclude": ["node_modules", "dist", "**/*.spec.ts"]
}
Enter fullscreen mode Exit fullscreen mode

Em seguida, altere o script start (não precisaremos mais do src ali) e o de build.

Adicione também o script postbuild no package.json:

{
  "scripts": {
    "start": "node dist/lib/index.js",
    "build": "babel src --extensions \".js,.ts\" --out-dir dist/lib --copy-files --no-copy-ignored",
    "postbuild": "tsc -p tsconfig.build.json --emitDeclarationOnly"
  }
}
Enter fullscreen mode Exit fullscreen mode

Vamos apagar a pasta dist gerada anteriormente, construir o projeto com o Babel, e então rodar o comando de produção:

rm -rf dist && yarn build && yarn start
Enter fullscreen mode Exit fullscreen mode
yarn run v1.22.5
$ babel src --extensions ".js,.ts" --out-dir dist/lib --copy-files --no-copy-ignored
Successfully compiled 3 files with Babel (704ms).
$ tsc -p tsconfig.build.json --emitDeclarationOnly
✨  Done in 3.74s.
yarn run v1.22.5
$ node dist/lib/index.js
Hi! I'm bar method from BarService :)
✨  Done in 0.35s.
Enter fullscreen mode Exit fullscreen mode

E Voilà!

Foi bastante coisa feita aqui! Espero que você tenha conseguido usar essa funcionalidade massa nos seus projetos, e entendido cada detalhe da configuração. No final desse artigo eu deixo o exemplo completo que desenvolvemos juntos!

Finalizando…

Bem, é isso, por hoje é só!

Quero agradecer a você que chegou até aqui, e queria lhe pedir também para encaminhar-me as suas dúvidas, comentários, críticas, correções ou sugestões sobre a postagem.

Deixe seu ❤️ se gostou ou um 🦄 se esse post te ajudou de alguma maneira! Não se esqueça de ver os posts anteriores e me siga para maaaais conteúdos.

Até!

GitHub logo wnqueiroz / typescript-tsconfig-paths

An example project on how to configure tsconfig-paths

Top comments (2)

Collapse
 
tadeubdev profile image
Tadeu Barbosa

Thank you so much! You was helped me a lot!

Collapse
 
wnqueiroz profile image
William Queiroz

You're welcome Tadeu!