Português

Desvende o poder dos tipos utilitários do TypeScript para escrever código mais limpo, manutenível e com segurança de tipos. Explore aplicações práticas com exemplos do mundo real para desenvolvedores em todo o mundo.

Dominando os Tipos Utilitários do TypeScript: Um Guia Prático para Desenvolvedores Globais

O TypeScript oferece um poderoso conjunto de tipos utilitários integrados que podem melhorar significativamente a segurança de tipos, a legibilidade e a manutenibilidade do seu código. Estes tipos utilitários são essencialmente transformações de tipo predefinidas que pode aplicar a tipos existentes, poupando-lhe a escrita de código repetitivo e propenso a erros. Este guia irá explorar vários tipos utilitários com exemplos práticos que se aplicam a desenvolvedores em todo o mundo.

Porquê Usar Tipos Utilitários?

Os tipos utilitários abordam cenários comuns de manipulação de tipos. Ao utilizá-los, pode:

Tipos Utilitários Principais

Partial<T>

Partial<T> constrói um tipo onde todas as propriedades de T são definidas como opcionais. Isto é particularmente útil quando se quer criar um tipo para atualizações parciais ou objetos de configuração.

Exemplo:

Imagine que está a construir uma plataforma de e-commerce com clientes de diversas regiões. Tem um tipo Customer:


interface Customer {
  id: string;
  firstName: string;
  lastName: string;
  email: string;
  phoneNumber: string;
  address: {
    street: string;
    city: string;
    country: string;
    postalCode: string;
  };
  preferences?: {
    language: string;
    currency: string;
  }
}

Ao atualizar as informações de um cliente, pode não querer exigir todos os campos. Partial<Customer> permite-lhe definir um tipo onde todas as propriedades de Customer são opcionais:


type PartialCustomer = Partial<Customer>;

function updateCustomer(id: string, updates: PartialCustomer): void {
  // ... implementação para atualizar o cliente com o ID fornecido
}

updateCustomer("123", { firstName: "John", lastName: "Doe" }); // Válido
updateCustomer("456", { address: { city: "London" } }); // Válido

Readonly<T>

Readonly<T> constrói um tipo onde todas as propriedades de T são definidas como readonly, impedindo a sua modificação após a inicialização. Isto é valioso para garantir a imutabilidade.

Exemplo:

Considere um objeto de configuração para a sua aplicação global:


interface AppConfig {
  apiUrl: string;
  theme: string;
  supportedLanguages: string[];
  version: string; // Versão adicionada
}

const config: AppConfig = {
  apiUrl: "https://api.example.com",
  theme: "dark",
  supportedLanguages: ["en", "fr", "de", "es", "zh"],
  version: "1.0.0"
};

Para evitar a modificação acidental da configuração após a inicialização, pode usar Readonly<AppConfig>:


type ReadonlyAppConfig = Readonly<AppConfig>;

const readonlyConfig: ReadonlyAppConfig = {
  apiUrl: "https://api.example.com",
  theme: "dark",
  supportedLanguages: ["en", "fr", "de", "es", "zh"],
  version: "1.0.0"
};

// readonlyConfig.apiUrl = "https://newapi.example.com"; // Erro: Não é possível atribuir a 'apiUrl' porque é uma propriedade somente de leitura.

Pick<T, K>

Pick<T, K> constrói um tipo ao selecionar o conjunto de propriedades K de T, onde K é uma união de tipos literais de string que representam os nomes das propriedades que deseja incluir.

Exemplo:

Digamos que tem uma interface Event com várias propriedades:


interface Event {
  id: string;
  title: string;
  description: string;
  location: string;
  startTime: Date;
  endTime: Date;
  organizer: string;
  attendees: string[];
}

Se precisar apenas de title, location e startTime para um componente de exibição específico, pode usar Pick:


type EventSummary = Pick<Event, "title" | "location" | "startTime">;

function displayEventSummary(event: EventSummary): void {
  console.log(`Event: ${event.title} at ${event.location} on ${event.startTime}`);
}

Omit<T, K>

Omit<T, K> constrói um tipo excluindo o conjunto de propriedades K de T, onde K é uma união de tipos literais de string que representam os nomes das propriedades que deseja excluir. É o oposto de Pick.

Exemplo:

Usando a mesma interface Event, se quiser criar um tipo para criar novos eventos, pode querer excluir a propriedade id, que é normalmente gerada pelo backend:


type NewEvent = Omit<Event, "id">;

function createEvent(event: NewEvent): void {
  // ... implementação para criar um novo evento
}

Record<K, T>

Record<K, T> constrói um tipo de objeto cujas chaves de propriedade são K e cujos valores de propriedade são T. K pode ser uma união de tipos literais de string, tipos literais de número ou um símbolo. Isto é perfeito para criar dicionários ou mapas.

Exemplo:

Imagine que precisa de armazenar traduções para a interface do utilizador da sua aplicação. Pode usar Record para definir um tipo para as suas traduções:


type Translations = Record<string, string>;

const enTranslations: Translations = {
  "hello": "Hello",
  "goodbye": "Goodbye",
  "welcome": "Welcome to our platform!"
};

const frTranslations: Translations = {
  "hello": "Bonjour",
  "goodbye": "Au revoir",
  "welcome": "Bienvenue sur notre plateforme !"
};

function translate(key: string, language: string): string {
  const translations = language === "en" ? enTranslations : frTranslations; //Simplificado
  return translations[key] || key; // Recorre à chave se nenhuma tradução for encontrada
}

console.log(translate("hello", "en")); // Saída: Hello
console.log(translate("hello", "fr")); // Saída: Bonjour
console.log(translate("nonexistent", "en")); // Saída: nonexistent

Exclude<T, U>

Exclude<T, U> constrói um tipo excluindo de T todos os membros da união que são atribuíveis a U. É útil para filtrar tipos específicos de uma união.

Exemplo:

Pode ter um tipo que representa diferentes tipos de eventos:


type EventType = "concert" | "conference" | "workshop" | "webinar";

Se quiser criar um tipo que exclua eventos do tipo "webinar", pode usar Exclude:


type PhysicalEvent = Exclude<EventType, "webinar">;

// PhysicalEvent é agora "concert" | "conference" | "workshop"

function attendPhysicalEvent(event: PhysicalEvent): void {
  console.log(`Attending a ${event}`);
}

// attendPhysicalEvent("webinar"); // Erro: O argumento do tipo '"webinar"' não é atribuível ao parâmetro do tipo '"concert" | "conference" | "workshop"'.

attendPhysicalEvent("concert"); // Válido

Extract<T, U>

Extract<T, U> constrói um tipo extraindo de T todos os membros da união que são atribuíveis a U. É o oposto de Exclude.

Exemplo:

Usando o mesmo EventType, pode extrair o tipo de evento webinar:


type OnlineEvent = Extract<EventType, "webinar">;

// OnlineEvent é agora "webinar"

function attendOnlineEvent(event: OnlineEvent): void {
  console.log(`Attending a ${event} online`);
}

attendOnlineEvent("webinar"); // Válido
// attendOnlineEvent("concert"); // Erro: O argumento do tipo '"concert"' não é atribuível ao parâmetro do tipo '"webinar"'.

NonNullable<T>

NonNullable<T> constrói um tipo excluindo null e undefined de T.

Exemplo:


type MaybeString = string | null | undefined;

type DefinitelyString = NonNullable<MaybeString>;

// DefinitelyString é agora string

function processString(str: DefinitelyString): void {
  console.log(str.toUpperCase());
}

// processString(null); // Erro: O argumento do tipo 'null' não é atribuível ao parâmetro do tipo 'string'.
// processString(undefined); // Erro: O argumento do tipo 'undefined' não é atribuível ao parâmetro do tipo 'string'.
processString("hello"); // Válido

ReturnType<T>

ReturnType<T> constrói um tipo que consiste no tipo de retorno da função T.

Exemplo:


function greet(name: string): string {
  return `Hello, ${name}!`;
}

type Greeting = ReturnType<typeof greet>;

// Greeting é agora string

const message: Greeting = greet("World");

console.log(message);

Parameters<T>

Parameters<T> constrói um tipo de tupla a partir dos tipos dos parâmetros de um tipo de função T.

Exemplo:


function logEvent(eventName: string, eventData: object): void {
  console.log(`Event: ${eventName}`, eventData);
}

type LogEventParams = Parameters<typeof logEvent>;

// LogEventParams é agora [eventName: string, eventData: object]

const params: LogEventParams = ["user_login", { userId: "123", timestamp: Date.now() }];

logEvent(...params);

ConstructorParameters<T>

ConstructorParameters<T> constrói um tipo de tupla ou array a partir dos tipos dos parâmetros de um tipo de função construtora T. Ele infere os tipos dos argumentos que precisam ser passados para o construtor de uma classe.

Exemplo:


class Greeter {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  greet() {
    return "Hello, " + this.greeting;
  }
}


type GreeterParams = ConstructorParameters<typeof Greeter>;

// GreeterParams é agora [message: string]

const paramsGreeter: GreeterParams = ["World"];
const greeterInstance = new Greeter(...paramsGreeter);

console.log(greeterInstance.greet()); // Saída: Hello, World

Required<T>

Required<T> constrói um tipo que consiste em todas as propriedades de T definidas como obrigatórias. Torna todas as propriedades opcionais em obrigatórias.

Exemplo:


interface UserProfile {
  name: string;
  age?: number;
  email?: string;
}

type RequiredUserProfile = Required<UserProfile>;

// RequiredUserProfile é agora { name: string; age: number; email: string; }

const completeProfile: RequiredUserProfile = {
  name: "Alice",
  age: 30,
  email: "alice@example.com"
};

// const incompleteProfile: RequiredUserProfile = { name: "Bob" }; // Erro: A propriedade 'age' está em falta no tipo '{ name: string; }', mas é obrigatória no tipo 'Required'.

Tipos Utilitários Avançados

Tipos de Template Literal

Os tipos de template literal permitem construir novos tipos literais de string concatenando tipos literais de string existentes, tipos literais de número e mais. Isto permite uma poderosa manipulação de tipos baseada em strings.

Exemplo:


type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = `/api/users` | `/api/products`;

type RequestURL = `${HTTPMethod} ${APIEndpoint}`;

// RequestURL é agora "GET /api/users" | "POST /api/users" | "PUT /api/users" | "DELETE /api/users" | "GET /api/products" | "POST /api/products" | "PUT /api/products" | "DELETE /api/products"

function makeRequest(url: RequestURL): void {
  console.log(`Making request to ${url}`);
}

makeRequest("GET /api/users"); // Válido
// makeRequest("INVALID /api/users"); // Erro

Tipos Condicionais

Os tipos condicionais permitem definir tipos que dependem de uma condição expressa como uma relação de tipos. Eles usam a palavra-chave infer para extrair informações de tipo.

Exemplo:


type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

// Se T for uma Promise, então o tipo é U; caso contrário, o tipo é T.

async function fetchData(): Promise<number> {
  return 42;
}


type Data = UnwrapPromise<ReturnType<typeof fetchData>>;

// Data é agora number

function processData(data: Data): void {
  console.log(data * 2);
}

processData(await fetchData());

Aplicações Práticas e Cenários do Mundo Real

Vamos explorar cenários mais complexos do mundo real onde os tipos utilitários se destacam.

1. Manipulação de Formulários

Ao lidar com formulários, muitas vezes tem cenários onde precisa de representar os valores iniciais do formulário, os valores atualizados e os valores finais submetidos. Os tipos utilitários podem ajudá-lo a gerir estes diferentes estados de forma eficiente.


interface FormData {
  firstName: string;
  lastName: string;
  email: string;
  country: string; // Obrigatório
  city?: string; // Opcional
  postalCode?: string;
  newsletterSubscription?: boolean;
}

// Valores iniciais do formulário (campos opcionais)
type InitialFormValues = Partial<FormData>;

// Valores atualizados do formulário (alguns campos podem estar em falta)
type UpdatedFormValues = Partial<FormData>;

// Campos obrigatórios para submissão
type RequiredForSubmission = Required<Pick<FormData, 'firstName' | 'lastName' | 'email' | 'country'>>;

// Use estes tipos nos seus componentes de formulário
function initializeForm(initialValues: InitialFormValues): void { }
function updateForm(updates: UpdatedFormValues): void {}
function submitForm(data: RequiredForSubmission): void {}


const initialForm: InitialFormValues = { newsletterSubscription: true };

const updateFormValues: UpdatedFormValues = {
    firstName: "John",
    lastName: "Doe"
};

// const submissionData: RequiredForSubmission = { firstName: "test", lastName: "test", email: "test" }; // ERRO: Falta 'country' 
const submissionData: RequiredForSubmission = { firstName: "test", lastName: "test", email: "test", country: "USA" }; //OK


2. Transformação de Dados da API

Ao consumir dados de uma API, pode precisar de transformar os dados para um formato diferente para a sua aplicação. Os tipos utilitários podem ajudá-lo a definir a estrutura dos dados transformados.


interface APIResponse {
  user_id: string;
  first_name: string;
  last_name: string;
  email_address: string;
  profile_picture_url: string;
  is_active: boolean;
}

// Transformar a resposta da API para um formato mais legível
type UserData = {
  id: string;
  fullName: string;
  email: string;
  avatar: string;
  active: boolean;
};

function transformApiResponse(response: APIResponse): UserData {
  return {
    id: response.user_id,
    fullName: `${response.first_name} ${response.last_name}`,
    email: response.email_address,
    avatar: response.profile_picture_url,
    active: response.is_active
  };
}

function fetchAndTransformData(url: string): Promise<UserData> {
    return fetch(url)
        .then(response => response.json())
        .then(data => transformApiResponse(data));
}


// Pode até mesmo forçar o tipo através de:

function saferTransformApiResponse(response: APIResponse): UserData {
    const {user_id, first_name, last_name, email_address, profile_picture_url, is_active} = response;
    const transformed: UserData = {
        id: user_id,
        fullName: `${first_name} ${last_name}`,
        email: email_address,
        avatar: profile_picture_url,
        active: is_active
    };

    return transformed;
}

3. Manipulação de Objetos de Configuração

Objetos de configuração são comuns em muitas aplicações. Os tipos utilitários podem ajudá-lo a definir a estrutura do objeto de configuração e garantir que ele seja usado corretamente.


interface AppSettings {
  theme: "light" | "dark";
  language: string;
  notificationsEnabled: boolean;
  apiUrl?: string; // URL da API opcional para diferentes ambientes
  timeout?: number;  //Opcional
}

// Configurações padrão
const defaultSettings: AppSettings = {
  theme: "light",
  language: "en",
  notificationsEnabled: true
};

// Função para mesclar as configurações do utilizador com as configurações padrão
function mergeSettings(userSettings: Partial<AppSettings>): AppSettings {
  return { ...defaultSettings, ...userSettings };
}

// Use as configurações mescladas na sua aplicação
const mergedSettings = mergeSettings({ theme: "dark", apiUrl: "https://customapi.example.com" });
console.log(mergedSettings);

Dicas para o Uso Eficaz de Tipos Utilitários

  • Comece de forma simples: Comece com tipos utilitários básicos como Partial e Readonly antes de avançar para os mais complexos.
  • Use nomes descritivos: Dê aos seus aliases de tipo nomes significativos para melhorar a legibilidade.
  • Combine tipos utilitários: Pode combinar múltiplos tipos utilitários para alcançar transformações de tipo complexas.
  • Aproveite o suporte do editor: Tire partido do excelente suporte do editor do TypeScript para explorar os efeitos dos tipos utilitários.
  • Entenda os conceitos subjacentes: Um entendimento sólido do sistema de tipos do TypeScript é essencial para o uso eficaz dos tipos utilitários.

Conclusão

Os tipos utilitários do TypeScript são ferramentas poderosas que podem melhorar significativamente a qualidade e a manutenibilidade do seu código. Ao entender e aplicar estes tipos utilitários de forma eficaz, pode escrever aplicações mais limpas, com maior segurança de tipos e mais robustas que atendam às demandas de um cenário de desenvolvimento global. Este guia forneceu uma visão abrangente dos tipos utilitários comuns e exemplos práticos. Experimente com eles e explore o seu potencial para aprimorar os seus projetos TypeScript. Lembre-se de priorizar a legibilidade e a clareza ao usar tipos utilitários, e esforce-se sempre para escrever código que seja fácil de entender e manter, não importa onde os seus colegas desenvolvedores estejam localizados.