kb.erickguedes.com
TypeScript: Tipagem e Produtividade

Generics e Tipos Avançados

Aula 4 de 7

Generics Básicos

Generics permitem criar componentes que funcionam com múltiplos tipos.

Função Genérica

function primeiroElemento<T>(arr: T[]): T | undefined {
  return arr[0];
}

// Inferência automática
const num = primeiroElemento([1, 2, 3]);    // number | undefined
const str = primeiroElemento(["a", "b"]);   // string | undefined

// Especificação explícita
const item = primeiroElemento<number>([1, 2, 3]);

Múltiplos Type Parameters

function par<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

const resultado = par("idade", 30); // [string, number]

Classe Genérica

class Pilha<T> {
  private items: T[] = [];

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

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

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get tamanho(): number {
    return this.items.length;
  }
}

const pilhaNumeros = new Pilha<number>();
pilhaNumeros.push(1);
pilhaNumeros.push(2);
console.log(pilhaNumeros.pop()); // 2

Interface Genérica

interface Repositorio<T> {
  buscar(id: string): Promise<T | null>;
  listar(): Promise<T[]>;
  criar(item: T): Promise<T>;
  atualizar(id: string, item: Partial<T>): Promise<T>;
  deletar(id: string): Promise<boolean>;
}

interface Usuario {
  id: string;
  nome: string;
  email: string;
}

class RepositorioUsuario implements Repositorio<Usuario> {
  async buscar(id: string): Promise<Usuario | null> {
    return null;
  }
  async listar(): Promise<Usuario[]> {
    return [];
  }
  async criar(item: Usuario): Promise<Usuario> {
    return item;
  }
  async atualizar(id: string, item: Partial<Usuario>): Promise<Usuario> {
    return { id, ...item } as Usuario;
  }
  async deletar(id: string): Promise<boolean> {
    return true;
  }
}

Generic Constraints (extends)

Restringir quais tipos são aceitos:

interface ComComprimento {
  length: number;
}

function logComprimento<T extends ComComprimento>(item: T): void {
  console.log(item.length);
}

logComprimento("texto");          // 5
logComprimento([1, 2, 3]);        // 3
// logComprimento(123);           // Erro: number não tem length

keyof Constraint

function getPropriedade<T, K extends keyof T>(obj: T, chave: K): T[K] {
  return obj[chave];
}

const user = { nome: "Alice", idade: 30 };
getPropriedade(user, "nome");    // string
getPropriedade(user, "idade");   // number
// getPropriedade(user, "email"); // Erro: "email" não está em keyof T

Conditional Types

type IsString<T> = T extends string ? "sim" : "nao";

type A = IsString<string>; // "sim"
type B = IsString<number>; // "nao"

// Encadeado
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type T0 = TypeName<string>;  // "string"
type T1 = TypeName<true>;    // "boolean"

Distribuição em Union Types

type ToArray<T> = T extends unknown ? T[] : never;

type Result = ToArray<string | number>;
// Result: string[] | number[] (não (string | number)[])

Mapped Types

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

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

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

// Exemplo:
interface Usuario {
  nome: string;
  idade: number;
}

type UsuarioOpcional = Opcional<Usuario>;
// { nome?: string; idade?: number }

Mapped Types com Modificadores

type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

type Setters<T> = {
  [P in keyof T as `set${Capitalize<string & P>}`]: (valor: T[P]) => void;
};

type UsuarioGetters = Getters<Usuario>;
// { getNome: () => string; getIdade: () => number }

Template Literal Types

type Evento = "click" | "focus" | "blur";
type Handler = `on${Capitalize<Evento>}`;
// "onClick" | "onFocus" | "onBlur"

type Margem = "margin";
type Lado = "top" | "right" | "bottom" | "left";
type PropriedadeMargem = `${Margem}${Capitalize<Lado>}`;
// "marginTop" | "marginRight" | "marginBottom" | "marginLeft"

infer Keyword

Usado em conditional types para inferir tipos dentro de padrões:

type Retorno<T> = T extends (...args: unknown[]) => infer R ? R : never;

type Fn = (x: number) => string;
type R = Retorno<Fn>; // string

// Inferir parâmetros
type Parametros<T> = T extends (...args: infer P) => unknown ? P : never;

type P = Parametros<(a: string, b: number) => void>;
// [string, number]

// Inferir tipo de Promise
type Await<T> = T extends Promise<infer U> ? U : T;

type AsyncResult = Await<Promise<string>>; // string
type SyncResult = Await<number>;           // number

Lab: Helpers Genéricos

// Deep Partial — recursivo
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface Config {
  db: { host: string; port: number };
  cache: { ttl: number; enabled: boolean };
}

type PartialConfig = DeepPartial<Config>;
// Tudo opcional, recursivamente

// PickByType — seleciona props por tipo
type PickByType<T, V> = {
  [P in keyof T as T[P] extends V ? P : never]: T[P];
};

type ApenasStrings = PickByType<Config, string>;
// { db: { host: string; port: number }; cache: { ttl: number; enabled: boolean } }

// Função type-safe com validação
function criarObjeto<T extends Record<string, unknown>>(
  schema: Record<keyof T, string>,
  dados: T,
): T {
  for (const chave of Object.keys(schema)) {
    if (typeof dados[chave] !== schema[chave]) {
      throw new Error(`Tipo inválido para ${chave}`);
    }
  }
  return dados;
}
npx tsc --noEmit

Generics + conditional types + mapped types formam a base do sistema de tipos mais expressivo do TypeScript. Use infer para extrair tipos de dentro de outros tipos.