TypeScript no frontend: Vite e configuração moderna

1. Por que Vite com TypeScript?

O ecossistema frontend enfrentou por anos um problema crônico: a lentidão dos bundlers tradicionais. Webpack, embora extremamente flexível, exigia segundos (ou minutos) para reiniciar o servidor de desenvolvimento após qualquer alteração. Parcel melhorava a experiência, mas ainda sofria com projetos grandes.

Vite surgiu como resposta a esse gargalo. Em vez de empacotar todo o código antes de servir, ele usa ESBuild para transformação rápida de TypeScript durante o desenvolvimento — uma ferramenta escrita em Go que compila arquivos dezenas de vezes mais rápido que o tsc tradicional. Para produção, o Vite delega o empacotamento ao Rollup, garantindo bundles otimizados.

A diferença prática é imediata: enquanto o Webpack recompila o projeto inteiro a cada salvamento, o Vite utiliza Hot Module Replacement (HMR) baseado em módulos ES nativos, atualizando apenas o arquivo modificado. O resultado? Um servidor de desenvolvimento que inicia em milissegundos e responde instantaneamente.

// Exemplo: diferença na velocidade de compilação
// Com tsc tradicional (lento para projetos grandes)
// tsc --noEmit --watch

// Com Vite + ESBuild (rápido, nativo)
// vite dev

2. Configuração inicial de um projeto Vite + TypeScript

Criar um projeto novo é simples:

npm create vite@latest meu-projeto -- --template react-ts
cd meu-projeto
npm install

A estrutura gerada já vem otimizada:

meu-projeto/
├── src/
│   ├── App.tsx
│   ├── main.tsx
│   └── vite-env.d.ts
├── public/
├── types/
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

O diretório types/ não é criado por padrão, mas é uma boa prática adicioná-lo para declarações de tipos globais e interfaces compartilhadas.

3. Tipagem avançada no tsconfig.json para frontend moderno

O tsconfig.json é o coração da configuração TypeScript. Para projetos Vite, recomenda-se:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "lib": ["ES2020", "DOM", "DOM.Iterable"]
  }
}

Explicação das opções:

  • "moduleResolution": "bundler": essencial para Vite, pois permite que o TypeScript entenda imports de arquivos não-JS (CSS, imagens) sem declarar explicitamente cada um.
  • "noUncheckedIndexedAccess": true: previne erros ao acessar arrays ou objetos com índices dinâmicos sem verificação.
  • "exactOptionalPropertyTypes": true: garante que propriedades opcionais sejam exatamente undefined quando ausentes, não undefined | T.

Os paths permitem imports absolutos:

// Em vez de import { Button } from '../../components/Button'
import { Button } from '@/components/Button'

4. Integração de TypeScript com Vite: plugins e configurações

Para sincronizar TypeScript e Vite, três plugins são indispensáveis:

a) vite-plugin-checker — Validação de tipos em tempo real:

// vite.config.ts
import { defineConfig } from 'vite'
import checker from 'vite-plugin-checker'

export default defineConfig({
  plugins: [
    checker({ typescript: true })
  ]
})

Isso substitui o antigo fork-ts-checker-webpack-plugin, exibindo erros de tipo diretamente no terminal e no navegador.

b) vite-plugin-dts — Geração de arquivos .d.ts para bibliotecas:

import dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    dts({ rollupTypes: true })
  ]
})

c) Sincronizando resolve.alias com paths do TypeScript:

import { resolve } from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  }
})

5. Tipando recursos estáticos e assets no Vite

O arquivo vite-env.d.ts gerado automaticamente contém:

/// <reference types="vite/client" />

Isso adiciona tipos para imports de assets. Mas você pode estender para recursos específicos:

// src/types/assets.d.ts
declare module '*.svg' {
  import React from 'react'
  const SVGComponent: React.FC<React.SVGProps<SVGSVGElement>>
  export default SVGComponent
}

declare module '*.module.css' {
  const classes: { readonly [key: string]: string }
  export default classes
}

declare module '*.json' {
  const value: Record<string, unknown>
  export default value
}

Variáveis de ambiente tipadas:

// src/vite-env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string
  readonly VITE_APP_TITLE: string
  // Adicione outras variáveis aqui
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

Com Zod, você pode validar essas variáveis em tempo de execução:

import { z } from 'zod'

const envSchema = z.object({
  VITE_API_URL: z.string().url(),
  VITE_APP_TITLE: z.string().min(1)
})

const env = envSchema.parse(import.meta.env)

6. Linting e formatação com TypeScript no ecossistema Vite

ESLint moderno:

// eslint.config.js (flat config)
import tsParser from '@typescript-eslint/parser'
import tsPlugin from '@typescript-eslint/eslint-plugin'

export default [
  {
    files: ['src/**/*.ts', 'src/**/*.tsx'],
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        project: './tsconfig.json'
      }
    },
    plugins: {
      '@typescript-eslint': tsPlugin
    },
    rules: {
      '@typescript-eslint/no-unused-vars': 'error',
      '@typescript-eslint/explicit-function-return-type': 'warn'
    }
  }
]

Prettier integrado:

// .prettierrc
{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "es5",
  "printWidth": 100,
  "tabWidth": 2
}

Husky + lint-staged:

npx husky init
echo "npx lint-staged" > .husky/pre-commit
// package.json
{
  "lint-staged": {
    "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,css,md}": ["prettier --write"]
  }
}

7. Deploy e build otimizado com TypeScript

É crucial entender a diferença entre tsc e vite build:

  • tsc --noEmit: realiza type-checking completo, mas não gera arquivos JS.
  • vite build: transpila TypeScript via ESBuild (ignorando tipos) e empacota com Rollup.

No package.json, configure scripts separados:

{
  "scripts": {
    "dev": "vite",
    "build": "tsc --noEmit && vite build",
    "preview": "vite preview"
  }
}

Tree-shaking com TypeScript: O Rollup elimina automaticamente código morto. Para maximizar isso:

// ❌ Evite imports de efeito colateral
import './polyfills'

// ✅ Prefira imports nomeados
import { debounce } from 'lodash-es'

Configuração de lib para navegadores alvo:

// tsconfig.json
{
  "compilerOptions": {
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "target": "ES2020"
  }
}

E no vite.config.ts:

export default defineConfig({
  build: {
    target: 'es2020',
    polyfillDynamicImport: false
  }
})

Isso garante que o código gerado seja compatível com navegadores modernos (Chrome 80+, Firefox 75+, Safari 13.1+), sem polifills desnecessários.


Referências