TypeScript

Guia Completo de CLI com TypeScript: Construindo Ferramentas de Linha de Comando Tipadas

12 min de leitura

Guia Completo de CLI com TypeScript: Construindo Ferramentas de Linha de Comando Tipadas

Introdução: Por que TypeScript em CLIs? Quando você desenvolve aplicações de linha de comando (CLIs), trabalha com código que interage diretamente com usuários através do terminal. Diferentemente de aplicações web, onde você tem abstrações robustas e bibliotecas estabelecidas, CLIs exigem uma comunicação direta: parsing de argumentos, validação de entrada, tratamento de erros em tempo real. TypeScript traz segurança de tipo a esse cenário, capturando erros em tempo de compilação em vez de falhas em produção. A tipagem estática em TypeScript permite que você construa CLIs mais confiáveis, com autocompletar e refatoração segura. Você saberá exatamente qual forma seus argumentos devem ter, quais campos são opcionais, e como estruturar a resposta para o usuário. Isso reduz bugs e torna a manutenção significativamente mais fácil quando você retorna ao código meses depois. Fundamentos: Estruturando seu Projeto CLI Configuração Inicial e Dependências Antes de escrever a primeira linha de código, você precisa configurar seu ambiente. Um projeto CLI em TypeScript típico requer ferramentas

<h2>Introdução: Por que TypeScript em CLIs?</h2>

<p>Quando você desenvolve aplicações de linha de comando (CLIs), trabalha com código que interage diretamente com usuários através do terminal. Diferentemente de aplicações web, onde você tem abstrações robustas e bibliotecas estabelecidas, CLIs exigem uma comunicação direta: parsing de argumentos, validação de entrada, tratamento de erros em tempo real. TypeScript traz segurança de tipo a esse cenário, capturando erros em tempo de compilação em vez de falhas em produção.</p>

<p>A tipagem estática em TypeScript permite que você construa CLIs mais confiáveis, com autocompletar e refatoração segura. Você saberá exatamente qual forma seus argumentos devem ter, quais campos são opcionais, e como estruturar a resposta para o usuário. Isso reduz bugs e torna a manutenção significativamente mais fácil quando você retorna ao código meses depois.</p>

<h2>Fundamentos: Estruturando seu Projeto CLI</h2>

<h3>Configuração Inicial e Dependências</h3>

<p>Antes de escrever a primeira linha de código, você precisa configurar seu ambiente. Um projeto CLI em TypeScript típico requer ferramentas de build, um runner para executar durante desenvolvimento, e libraries para parsing de argumentos.</p>

<pre><code class="language-bash">mkdir meu-cli

cd meu-cli

npm init -y

npm install --save-dev typescript ts-node @types/node

npm install yargs chalk</code></pre>

<p>O <code>yargs</code> é a biblioteca mais robusta para parsing de argumentos com TypeScript, oferecendo validação automática e geração de help text. O <code>chalk</code> fornece cores para output terminal. Aqui está o <code>tsconfig.json</code> essencial:</p>

<pre><code class="language-json">{

&quot;compilerOptions&quot;: {

&quot;target&quot;: &quot;ES2020&quot;,

&quot;module&quot;: &quot;commonjs&quot;,

&quot;lib&quot;: [&quot;ES2020&quot;],

&quot;outDir&quot;: &quot;./dist&quot;,

&quot;rootDir&quot;: &quot;./src&quot;,

&quot;strict&quot;: true,

&quot;esModuleInterop&quot;: true,

&quot;skipLibCheck&quot;: true,

&quot;forceConsistentCasingInFileNames&quot;: true,

&quot;declaration&quot;: true

},

&quot;include&quot;: [&quot;src/*/&quot;],

&quot;exclude&quot;: [&quot;node_modules&quot;]

}</code></pre>

<h3>Estrutura de Pastas e Arquitetura</h3>

<p>A organização do projeto impacta diretamente na escalabilidade. Para um CLI pequeno, uma estrutura funcional é suficiente; para CLIs complexas com múltiplos comandos, você precisa de separação clara entre lógica de negócio e interface:</p>

<pre><code>meu-cli/

├── src/

│ ├── commands/

│ │ ├── deploy.ts

│ │ └── status.ts

│ ├── services/

│ │ └── api.ts

│ ├── types/

│ │ └── index.ts

│ └── index.ts

├── dist/

├── package.json

└── tsconfig.json</code></pre>

<p>Separar <code>commands</code> (interface com o usuário) de <code>services</code> (lógica real) permite reutilizar código em contextos diferentes e facilita testes. O diretório <code>types</code> centraliza suas definições TypeScript, evitando dispersão de interfaces pelo código.</p>

<h2>Criando Seu Primeiro CLI Funcional</h2>

<h3>Um CLI Simples com Yargs</h3>

<p>Vamos construir um gerenciador de tarefas básico que demonstra os conceitos fundamentais. Comece pelo arquivo principal:</p>

<pre><code class="language-typescript">// src/types/index.ts

export interface Task {

id: number;

title: string;

completed: boolean;

createdAt: Date;

}

export interface CommandResult {

success: boolean;

message: string;

data?: unknown;

}</code></pre>

<p>Agora, crie o serviço que contém a lógica de negócio:</p>

<pre><code class="language-typescript">// src/services/taskService.ts

import { Task, CommandResult } from &#039;../types/index&#039;;

let tasks: Task[] = [];

let nextId: number = 1;

export const taskService = {

addTask(title: string): CommandResult {

if (!title.trim()) {

return { success: false, message: &#039;Título não pode estar vazio&#039; };

}

const newTask: Task = {

id: nextId++,

title,

completed: false,

createdAt: new Date()

};

tasks.push(newTask);

return {

success: true,

message: Tarefa &quot;${title}&quot; adicionada com sucesso,

data: newTask

};

},

listTasks(): CommandResult {

return {

success: true,

message: Total de ${tasks.length} tarefas,

data: tasks

};

},

completeTask(id: number): CommandResult {

const task = tasks.find(t =&gt; t.id === id);

if (!task) {

return { success: false, message: Tarefa ${id} não encontrada };

}

task.completed = true;

return {

success: true,

message: Tarefa ${id} marcada como concluída,

data: task

};

}

};</code></pre>

<p>Agora defina os comandos que serão expostos ao usuário:</p>

<pre><code class="language-typescript">// src/commands/taskCommands.ts

import yargs from &#039;yargs&#039;;

import { hideBin } from &#039;yargs/helpers&#039;;

import chalk from &#039;chalk&#039;;

import { taskService } from &#039;../services/taskService&#039;;

export const setupTaskCommands = (argv: yargs.Argv) =&gt; {

return argv

.command(

&#039;add &lt;title&gt;&#039;,

&#039;Adicionar uma nova tarefa&#039;,

(yargs) =&gt; yargs.positional(&#039;title&#039;, {

describe: &#039;Título da tarefa&#039;,

type: &#039;string&#039;

}),

(args) =&gt; {

const result = taskService.addTask(args.title as string);

handleResult(result);

}

)

.command(

&#039;list&#039;,

&#039;Listar todas as tarefas&#039;,

() =&gt; {},

() =&gt; {

const result = taskService.listTasks();

if (result.success &amp;&amp; Array.isArray(result.data)) {

console.log(chalk.blue(&#039;=== Tarefas ===&#039;));

(result.data as any[]).forEach(task =&gt; {

const status = task.completed ? chalk.green(&#039;✓&#039;) : chalk.red(&#039;✗&#039;);

console.log(${status} [${task.id}] ${task.title});

});

} else {

console.log(chalk.yellow(result.message));

}

}

)

.command(

&#039;complete &lt;id&gt;&#039;,

&#039;Marcar tarefa como concluída&#039;,

(yargs) =&gt; yargs.positional(&#039;id&#039;, {

describe: &#039;ID da tarefa&#039;,

type: &#039;number&#039;

}),

(args) =&gt; {

const result = taskService.completeTask(args.id as number);

handleResult(result);

}

);

};

function handleResult(result: any): void {

if (result.success) {

console.log(chalk.green(✓ ${result.message}));

} else {

console.log(chalk.red(✗ ${result.message}));

process.exit(1);

}

}</code></pre>

<p>Por fim, integre tudo no ponto de entrada:</p>

<pre><code class="language-typescript">// src/index.ts

import yargs from &#039;yargs&#039;;

import { hideBin } from &#039;yargs/helpers&#039;;

import { setupTaskCommands } from &#039;./commands/taskCommands&#039;;

const main = async () =&gt; {

await setupTaskCommands(yargs(hideBin(process.argv)))

.demandCommand(1, &#039;Você deve especificar um comando&#039;)

.strict()

.help()

.parse();

};

main().catch((error) =&gt; {

console.error(&#039;Erro:&#039;, error.message);

process.exit(1);

});</code></pre>

<h3>Executando e Testando</h3>

<p>Adicione scripts no <code>package.json</code>:</p>

<pre><code class="language-json">{

&quot;scripts&quot;: {

&quot;dev&quot;: &quot;ts-node src/index.ts&quot;,

&quot;build&quot;: &quot;tsc&quot;,

&quot;start&quot;: &quot;node dist/index.js&quot;

}

}</code></pre>

<p>Teste seus comandos:</p>

<pre><code class="language-bash">npm run dev add &quot;Estudar TypeScript&quot;

npm run dev add &quot;Preparar apresentação&quot;

npm run dev list

npm run dev complete 1

npm run dev list</code></pre>

<h2>Padrões Avançados e Boas Práticas</h2>

<h3>Tratamento Robusto de Erros</h3>

<p>Em CLIs, o tratamento de erros não é apenas sobre capturar exceções. Você precisa validar entrada do usuário, comunicar problemas claramente e permitir recuperação quando possível.</p>

<pre><code class="language-typescript">// src/utils/errorHandler.ts

import chalk from &#039;chalk&#039;;

export class CLIError extends Error {

constructor(

public message: string,

public exitCode: number = 1

) {

super(message);

}

}

export const handleError = (error: unknown): void =&gt; {

if (error instanceof CLIError) {

console.error(chalk.red(Erro: ${error.message}));

process.exit(error.exitCode);

} else if (error instanceof Error) {

console.error(chalk.red(Erro inesperado: ${error.message}));

if (process.env.DEBUG) {

console.error(error.stack);

}

process.exit(1);

} else {

console.error(chalk.red(&#039;Erro desconhecido&#039;));

process.exit(1);

}

};</code></pre>

<p>Agora use isso em seus serviços:</p>

<pre><code class="language-typescript">// Exemplo de validação robusta

export const deleteTask = (id: number): CommandResult =&gt; {

if (!Number.isInteger(id) || id &lt;= 0) {

throw new CLIError(&#039;ID deve ser um número inteiro positivo&#039;);

}

const taskIndex = tasks.findIndex(t =&gt; t.id === id);

if (taskIndex === -1) {

throw new CLIError(Tarefa com ID ${id} não encontrada);

}

const [deletedTask] = tasks.splice(taskIndex, 1);

return {

success: true,

message: Tarefa &quot;${deletedTask.title}&quot; removida,

data: deletedTask

};

};</code></pre>

<h3>Validação de Argumentos com Esquemas</h3>

<p>Yargs funciona bem, mas para projetos maiores, adicione validação com Zod para robustez máxima:</p>

<pre><code class="language-bash">npm install zod</code></pre>

<pre><code class="language-typescript">// src/types/schemas.ts

import { z } from &#039;zod&#039;;

export const AddTaskSchema = z.object({

title: z.string().min(1, &#039;Título não pode estar vazio&#039;).max(200),

priority: z.enum([&#039;low&#039;, &#039;medium&#039;, &#039;high&#039;]).optional().default(&#039;medium&#039;)

});

export type AddTaskInput = z.infer&lt;typeof AddTaskSchema&gt;;

export const validateAddTask = (data: unknown): AddTaskInput =&gt; {

return AddTaskSchema.parse(data);

};</code></pre>

<p>Use na validação:</p>

<pre><code class="language-typescript">.command(

&#039;add &lt;title&gt;&#039;,

&#039;Adicionar tarefa com prioridade&#039;,

(yargs) =&gt; yargs

.positional(&#039;title&#039;, { type: &#039;string&#039; })

.option(&#039;priority&#039;, {

type: &#039;string&#039;,

choices: [&#039;low&#039;, &#039;medium&#039;, &#039;high&#039;],

default: &#039;medium&#039;

}),

(args) =&gt; {

try {

const validated = validateAddTask({

title: args.title,

priority: args.priority

});

const result = taskService.addTask(validated.title, validated.priority);

handleResult(result);

} catch (error) {

if (error instanceof z.ZodError) {

console.error(chalk.red(&#039;Validação falhou:&#039;));

error.errors.forEach(err =&gt; {

console.error( - ${err.path.join(&#039;.&#039;)}: ${err.message});

});

process.exit(1);

}

throw error;

}

}

)</code></pre>

<h3>Persistência de Dados</h3>

<p>CLIs reais precisam persistir dados. Aqui está um exemplo com JSON simples (para produção, considere SQLite ou um banco real):</p>

<pre><code class="language-typescript">// src/services/storage.ts

import fs from &#039;fs/promises&#039;;

import path from &#039;path&#039;;

import { Task } from &#039;../types/index&#039;;

const DATA_DIR = path.join(process.cwd(), &#039;.task-cli-data&#039;);

const TASKS_FILE = path.join(DATA_DIR, &#039;tasks.json&#039;);

export const storage = {

async loadTasks(): Promise&lt;Task[]&gt; {

try {

const data = await fs.readFile(TASKS_FILE, &#039;utf-8&#039;);

return JSON.parse(data);

} catch {

return [];

}

},

async saveTasks(tasks: Task[]): Promise&lt;void&gt; {

await fs.mkdir(DATA_DIR, { recursive: true });

await fs.writeFile(TASKS_FILE, JSON.stringify(tasks, null, 2));

}

};</code></pre>

<p>Integre isso ao seu serviço:</p>

<pre><code class="language-typescript">// src/services/taskService.ts

import { storage } from &#039;./storage&#039;;

let tasks: Task[] = [];

export const taskService = {

async initialize(): Promise&lt;void&gt; {

tasks = await storage.loadTasks();

},

async addTask(title: string): Promise&lt;CommandResult&gt; {

// ... lógica anterior ...

await storage.saveTasks(tasks);

return result;

}

};</code></pre>

<p>E no seu <code>index.ts</code>:</p>

<pre><code class="language-typescript">const main = async () =&gt; {

await taskService.initialize();

await setupTaskCommands(yargs(hideBin(process.argv)))

.demandCommand(1)

.strict()

.help()

.parse();

};</code></pre>

<h2>Conclusão</h2>

<p>Você aprendeu que construir CLIs com TypeScript não é sobre usar a ferramenta mais moderna, mas sobre estruturar código legível e seguro desde o início. A tipagem estática captura erros cedo, a separação entre commands e services torna o código reutilizável, e padrões de validação robustos economizam debugging depois. Na prática, a maioria dos problemas em CLIs vem de entrada de usuário mal validada, então invista tempo em schemas de validação. Finalmente, sempre persista dados de forma previsível—não deixe seus usuários à deriva.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://yargs.js.org/" target="_blank" rel="noopener noreferrer">Yargs Documentation</a></li>

<li><a href="https://www.typescriptlang.org/docs/" target="_blank" rel="noopener noreferrer">TypeScript Handbook</a></li>

<li><a href="https://zod.dev/" target="_blank" rel="noopener noreferrer">Zod - TypeScript-first Schema Validation</a></li>

<li><a href="https://nodejs.org/api/fs.html" target="_blank" rel="noopener noreferrer">Node.js fs Documentation</a></li>

<li><a href="https://github.com/chalk/chalk" target="_blank" rel="noopener noreferrer">Chalk - Terminal String Styling</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

O que Todo Dev Deve Saber sobre Node.js com TypeScript: Configuração, tsx e ts-node na Prática
O que Todo Dev Deve Saber sobre Node.js com TypeScript: Configuração, tsx e ts-node na Prática

Node.js com TypeScript: Configuração, tsx e ts-node na Prática TypeScript é u...

Decorators em TypeScript: Class, Method, Property e Parameter na Prática
Decorators em TypeScript: Class, Method, Property e Parameter na Prática

O que são Decorators em TypeScript Decorators são uma feature experimental do...

Dominando NestJS Avançado: Guards, Interceptors, Pipes e Exception Filters em Projetos Reais
Dominando NestJS Avançado: Guards, Interceptors, Pipes e Exception Filters em Projetos Reais

Introdução ao Middleware de NestJS NestJS é um framework robusto construído s...