TypeScript Avançado: Tipos Genéricos e Utilitários que Transformam seu Código

Introdução Como desenvolvedor TypeScript, você provavelmente já domina os tipos básicos como string, number, e boolean. Talvez você já utilize interfaces, types e enums no seu dia a dia. Mas quando foi a última vez que você explorou o verdadeiro potencial dos tipos genéricos e utilitários do TypeScript? Neste post, vou compartilhar algumas das técnicas avançadas de tipagem que revolucionaram minha forma de programar e que transformaram completamente a qualidade e segurança do meu código. Vamos explorar como utilizar corretamente tipos genéricos, utility types nativos e até mesmo criar nossos próprios tipos utilitários personalizados. Se você quer levar suas habilidades em TypeScript para o próximo nível, continue lendo! Genéricos: A Superpotência do TypeScript Tipos genéricos são uma das ferramentas mais poderosas do TypeScript, permitindo criar componentes reutilizáveis que funcionam com uma variedade de tipos, sem sacrificar a segurança de tipos. Genéricos Básicos Vejamos primeiro um exemplo simples de uma função genérica: function identity(arg: T): T { return arg; } const num = identity(42); // num é inferido como number const str = identity("hello"); // str é inferido como string O TypeScript infere automaticamente o tipo com base no argumento passado. Mas onde os genéricos realmente brilham é em cenários mais complexos. Genéricos com Restrições Podemos restringir os tipos que podem ser usados com nossos genéricos: interface HasLength { length: number; } function logLength(arg: T): T { console.log(arg.length); // Seguro! Sabemos que T tem uma propriedade length return arg; } logLength("hello"); // string tem .length, então funciona logLength([1, 2, 3]); // arrays têm .length, então funciona logLength({ length: 10 }); // Objetos com length também funcionam // logLength(123); // Erro! number não tem propriedade length Genéricos em Classes Genéricos são extremamente úteis em classes, especialmente quando lidamos com estruturas de dados: class Queue { private data: T[] = []; push(item: T): void { this.data.push(item); } pop(): T | undefined { return this.data.shift(); } } const numberQueue = new Queue(); numberQueue.push(10); // numberQueue.push("10"); // Erro! A fila só aceita números Utility Types Nativos do TypeScript O TypeScript vem com vários tipos utilitários incorporados que resolvem problemas comuns. Vamos explorar alguns dos mais úteis: Partial Torna todas as propriedades de um tipo opcionais: interface User { id: number; name: string; email: string; role: string; } function updateUser(user: User, updates: Partial): User { return { ...user, ...updates }; } const user: User = { id: 1, name: "Johan", email: "johan@example.com", role: "developer" }; // Podemos atualizar apenas algumas propriedades const updatedUser = updateUser(user, { email: "johan.dev@example.com" }); Pick e Omit Selecionam ou removem propriedades de um tipo: type UserPublicInfo = Pick; // Equivalente a: { name: string; role: string; } type UserWithoutId = Omit; // Equivalente a: { name: string; email: string; role: string; } function displayUserProfile(userInfo: UserPublicInfo) { console.log(`${userInfo.name} - ${userInfo.role}`); } function createUser(userData: UserWithoutId): User { return { id: Math.random(), ...userData }; } Record Cria um tipo de objeto com chaves de um tipo e valores de outro: type Role = "admin" | "user" | "guest"; type RoleAccess = Record; const accessRights: RoleAccess = { admin: ["read", "write", "delete"], user: ["read", "write"], guest: ["read"] }; ReturnType Extrai o tipo de retorno de uma função: function fetchUserData(id: number) { return { id, name: "Johan", timestamp: new Date() }; } type UserData = ReturnType; // Equivalente a: { id: number; name: string; timestamp: Date; } function processUserData(data: UserData) { console.log(`Processing data for ${data.name}`); } Criando Seus Próprios Utility Types Uma das partes mais poderosas do TypeScript é poder criar seus próprios tipos utilitários para resolver problemas específicos do seu domínio. DeepPartial Quando Partial não é suficiente porque você precisa fazer propriedades aninhadas também serem opcionais: type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial; } : T; interface NestedConfig { server: { port: number; host: string; ssl: { enabled: boolean; cert: string; } }; database: { url: string; credentials: { username: string; password: string; } }; } function updateConfig(config: NestedConfig, updates: DeepPartial): NestedConfig { // lógica de merge profundo aqui return merge(config, updates); // imaginando que existe uma função

Apr 25, 2025 - 19:21
 0
TypeScript Avançado: Tipos Genéricos e Utilitários que Transformam seu Código

TypeScript Avançado

Introdução

Como desenvolvedor TypeScript, você provavelmente já domina os tipos básicos como string, number, e boolean. Talvez você já utilize interfaces, types e enums no seu dia a dia. Mas quando foi a última vez que você explorou o verdadeiro potencial dos tipos genéricos e utilitários do TypeScript?

Neste post, vou compartilhar algumas das técnicas avançadas de tipagem que revolucionaram minha forma de programar e que transformaram completamente a qualidade e segurança do meu código. Vamos explorar como utilizar corretamente tipos genéricos, utility types nativos e até mesmo criar nossos próprios tipos utilitários personalizados.

Se você quer levar suas habilidades em TypeScript para o próximo nível, continue lendo!

Genéricos: A Superpotência do TypeScript

Genéricos em TypeScript

Tipos genéricos são uma das ferramentas mais poderosas do TypeScript, permitindo criar componentes reutilizáveis que funcionam com uma variedade de tipos, sem sacrificar a segurança de tipos.

Genéricos Básicos

Vejamos primeiro um exemplo simples de uma função genérica:

function identity<T>(arg: T): T {
  return arg;
}

const num = identity(42);       // num é inferido como number
const str = identity("hello");  // str é inferido como string

O TypeScript infere automaticamente o tipo com base no argumento passado. Mas onde os genéricos realmente brilham é em cenários mais complexos.

Genéricos com Restrições

Podemos restringir os tipos que podem ser usados com nossos genéricos:

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length);  // Seguro! Sabemos que T tem uma propriedade length
  return arg;
}

logLength("hello");        // string tem .length, então funciona
logLength([1, 2, 3]);      // arrays têm .length, então funciona
logLength({ length: 10 }); // Objetos com length também funcionam
// logLength(123);         // Erro! number não tem propriedade length

Genéricos em Classes

Genéricos são extremamente úteis em classes, especialmente quando lidamos com estruturas de dados:

class Queue<T> {
  private data: T[] = [];

  push(item: T): void {
    this.data.push(item);
  }

  pop(): T | undefined {
    return this.data.shift();
  }
}

const numberQueue = new Queue<number>();
numberQueue.push(10);
// numberQueue.push("10"); // Erro! A fila só aceita números

Utility Types Nativos do TypeScript

Utility Types

O TypeScript vem com vários tipos utilitários incorporados que resolvem problemas comuns. Vamos explorar alguns dos mais úteis:

Partial

Torna todas as propriedades de um tipo opcionais:

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}

function updateUser(user: User, updates: Partial<User>): User {
  return { ...user, ...updates };
}

const user: User = {
  id: 1,
  name: "Johan",
  email: "johan@example.com",
  role: "developer"
};

// Podemos atualizar apenas algumas propriedades
const updatedUser = updateUser(user, {
  email: "johan.dev@example.com"
});

Pick e Omit

Selecionam ou removem propriedades de um tipo:

type UserPublicInfo = Pick<User, "name" | "role">;
// Equivalente a: { name: string; role: string; }

type UserWithoutId = Omit<User, "id">;
// Equivalente a: { name: string; email: string; role: string; }

function displayUserProfile(userInfo: UserPublicInfo) {
  console.log(`${userInfo.name} - ${userInfo.role}`);
}

function createUser(userData: UserWithoutId): User {
  return {
    id: Math.random(),
    ...userData
  };
}

Record

Cria um tipo de objeto com chaves de um tipo e valores de outro:

type Role = "admin" | "user" | "guest";
type RoleAccess = Record<Role, string[]>;

const accessRights: RoleAccess = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"]
};

ReturnType

Extrai o tipo de retorno de uma função:

function fetchUserData(id: number) {
  return {
    id,
    name: "Johan",
    timestamp: new Date()
  };
}

type UserData = ReturnType<typeof fetchUserData>;
// Equivalente a: { id: number; name: string; timestamp: Date; }

function processUserData(data: UserData) {
  console.log(`Processing data for ${data.name}`);
}

Criando Seus Próprios Utility Types

Custom Utility Types

Uma das partes mais poderosas do TypeScript é poder criar seus próprios tipos utilitários para resolver problemas específicos do seu domínio.

DeepPartial

Quando Partial não é suficiente porque você precisa fazer propriedades aninhadas também serem opcionais:

type DeepPartial<T> = T extends object ? {
  [P in keyof T]?: DeepPartial<T[P]>;
} : T;

interface NestedConfig {
  server: {
    port: number;
    host: string;
    ssl: {
      enabled: boolean;
      cert: string;
    }
  };
  database: {
    url: string;
    credentials: {
      username: string;
      password: string;
    }
  };
}

function updateConfig(config: NestedConfig, updates: DeepPartial<NestedConfig>): NestedConfig {
  // lógica de merge profundo aqui
  return merge(config, updates); // imaginando que existe uma função merge
}

// Agora podemos atualizar propriedades aninhadas
updateConfig(defaultConfig, {
  server: {
    ssl: {
      enabled: true
    }
  }
});

NonNullable

O TypeScript tem um NonNullable nativo, mas vamos ver como implementá-lo nós mesmos:

type CustomNonNullable<T> = T extends null | undefined ? never : T;

type NullableString = string | null | undefined;
type DefinitelyString = CustomNonNullable<NullableString>; // string

ConditionalType com infer

Podemos usar infer para extrair tipos de outros tipos:

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

type PromiseString = Promise<string>;
type ExtractedString = UnwrapPromise<PromiseString>; // string

type NotAPromise = number;
type StillNumber = UnwrapPromise<NotAPromise>; // number

Padrões Avançados com TypeScript

Advanced TypeScript Patterns

Discriminated Unions

Um dos padrões mais úteis para modelar estados em TypeScript:

type Success = {
  status: "success";
  data: {
    id: string;
    name: string;
  };
};

type Loading = {
  status: "loading";
};

type Error = {
  status: "error";
  error: {
    message: string;
    code: number;
  };
};

type ApiResponse = Success | Loading | Error;

function handleResponse(response: ApiResponse) {
  switch (response.status) {
    case "success":
      console.log(`Data loaded: ${response.data.name}`);
      break;
    case "loading":
      console.log("Loading...");
      break;
    case "error":
      console.log(`Error: ${response.error.message}`);
      break;
  }
}

Mapped Types

Transforme um tipo em outro mapeando suas propriedades:

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type Optional<T> = {
  [P in keyof T]?: T[P];
};

type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

interface User {
  id: number;
  name: string;
  email: string;
}

const readonlyUser: Readonly<User> = {
  id: 1,
  name: "Johan",
  email: "johan@example.com"
};

// readonlyUser.name = "John"; // Erro! name é somente leitura

Template Literal Types

Uma adição mais recente ao TypeScript são os tipos de template literal:

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = "users" | "posts" | "comments";
type ApiRoute = `/${Endpoint}/${string}`;
type HttpRequest = `${HttpMethod} ${ApiRoute}`;

const validRequest: HttpRequest = "GET /users/123";
// const invalidRequest: HttpRequest = "PATCH /users/123"; // Erro! PATCH não é um HttpMethod válido
// const invalidEndpoint: HttpRequest = "GET /invalid/123"; // Erro! invalid não é um Endpoint válido

Exemplos Práticos do Mundo Real

Real World TypeScript

Tipo para Funções React Hook

Vamos criar um tipo para funções hook React:

type UseStateHook<T> = () => [T, React.Dispatch<React.SetStateAction<T>>];

const useCounter: UseStateHook<number> = () => {
  return React.useState(0);
};

// Uso:
const [count, setCount] = useCounter();

API Client Tipado

Criando um cliente de API fortemente tipado:

interface ApiClient {
  get<T>(url: string): Promise<T>;
  post<T, U>(url: string, data: T): Promise<U>;
  put<T, U>(url: string, data: T): Promise<U>;
  delete<T>(url: string): Promise<T>;
}

type User = {
  id: number;
  name: string;
  email: string;
};

type CreateUserData = Omit<User, "id">;

// Uso:
async function fetchUser(api: ApiClient, id: number): Promise<User> {
  return await api.get<User>(`/users/${id}`);
}

async function createUser(api: ApiClient, userData: CreateUserData): Promise<User> {
  return await api.post<CreateUserData, User>('/users', userData);
}

Form State Management

Um exemplo de gerenciador de estado de formulário tipado:

type FormField<T> = {
  value: T;
  error?: string;
  touched: boolean;
  validate: (value: T) => string | undefined;
};

type FormState<T> = {
  [K in keyof T]: FormField<T[K]>;
};

interface UserFormData {
  name: string;
  email: string;
  age: number;
}

// Um exemplo de um estado de formulário tipado
const userForm: FormState<UserFormData> = {
  name: {
    value: '',
    touched: false,
    validate: (value) => value ? undefined : 'Nome é obrigatório'
  },
  email: {
    value: '',
    touched: false,
    validate: (value) => /@/.test(value) ? undefined : 'Email inválido'
  },
  age: {
    value: 0,
    touched: false,
    validate: (value) => value >= 18 ? undefined : 'Deve ser maior de 18 anos'
  }
};

Dicas para Usar Tipos Avançados com Eficiência

TypeScript Tips

  1. Não exagere: Nem tudo precisa de tipos complexos. Use tipos avançados apenas quando eles trouxerem benefícios reais.

  2. Documente seus tipos complexos: Adicione comentários JSDoc para explicar o que seus tipos personalizados fazem.

  3. Reutilize tipos: Crie uma biblioteca de tipos personalizados para seu projeto para evitar duplicação.

  4. Aprenda a ler mensagens de erro: Os erros do TypeScript podem ser detalhados e assustadores no início, mas entendê-los é crucial.

  5. Use o playground do TypeScript: Experimente seus tipos no playground antes de implementá-los.

Conclusão

TypeScript Evolution

Dominar tipos avançados no TypeScript pode parecer intimidador no início, mas o investimento compensa enormemente. Código bem tipado não só reduz erros em tempo de execução, mas também serve como documentação viva e torna o desenvolvimento em equipe muito mais tranquilo.

Os exemplos que compartilhei transformaram minha forma de trabalhar com TypeScript e espero que possam ajudar você também a escrever código mais seguro, expressivo e fácil de manter.

TypeScript continua evoluindo a cada versão, com novos recursos de tipagem que nos permitem expressar conceitos de programação complexos com segurança adicional. Ficar atualizado com essas novidades é fundamental para aproveitar todo o potencial da linguagem.

Qual sua técnica favorita de tipagem avançada? Você já implementou algum desses padrões em seus projetos? Deixe nos comentários!

Se você gostou deste conteúdo, me siga para mais posts sobre desenvolvimento fullstack, TypeScript e boas práticas.

Você também pode conferir meu portfólio ou me encontrar no LinkedIn.