[Medium] Genéricos (Generics)
1. What are Generics?
O que são genéricos?
Genéricos (Generics) são uma funcionalidade poderosa do TypeScript que permite criar componentes reutilizáveis capazes de lidar com múltiplos tipos em vez de apenas um único tipo.
Conceito central: Ao definir funções, interfaces ou classes, não se específica um tipo concreto previamente, mas sim no momento do uso.
Por que genéricos são necessários?
Problema sem genéricos:
// Problema: precisa escrever uma função para cada tipo
function getStringItem(arr: string[]): string {
return arr[0];
}
function getNumberItem(arr: number[]): number {
return arr[0];
}
function getBooleanItem(arr: boolean[]): boolean {
return arr[0];
}
Solução com genéricos:
// Uma função para todos os tipos
function getItem<T>(arr: T[]): T {
return arr[0];
}
getItem<string>(['a', 'b']); // string
getItem<number>([1, 2, 3]); // number
getItem<boolean>([true, false]); // boolean
2. Basic Generic Syntax
Sintaxe básica de genéricos
Funções genéricas
// Sintaxe: <T> representa o parâmetro de tipo
function identity<T>(arg: T): T {
return arg;
}
// Uso 1: especificar o tipo explicitamente
let output1 = identity<string>('hello'); // output1: string
// Uso 2: deixar o TypeScript inferir o tipo
let output2 = identity('hello'); // output2: string (inferência automática)
Interfaces genéricas
interface Box<T> {
value: T;
}
const stringBox: Box<string> = {
value: 'hello',
};
const numberBox: Box<number> = {
value: 42,
};
Classes genéricas
class Container<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
get(index: number): T {
return this.items[index];
}
}
const stringContainer = new Container<string>();
stringContainer.add('hello');
stringContainer.add('world');
const numberContainer = new Container<number>();
numberContainer.add(1);
numberContainer.add(2);
3. Generic Constraints
Restrições genéricas
Restrições básicas
Sintaxe: Uso da palavra-chave extends para restringir o tipo genérico.
// T deve ter a propriedade length
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
getLength('hello'); // ✅ 5
getLength([1, 2, 3]); // ✅ 3
getLength({ length: 10 }); // ✅ 10
getLength(42); // ❌ Erro: number não tem a propriedade length
Restrição com keyof
// K deve ser uma chave de T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = {
name: 'John',
age: 30,
email: 'john@example.com',
};
getProperty(user, 'name'); // ✅ 'John'
getProperty(user, 'age'); // ✅ 30
getProperty(user, 'id'); // ❌ Erro: 'id' não é uma chave de user
Múltiplas restrições
// T deve satisfazer múltiplas condições simultaneamente
function process<T extends string | number>(value: T): T {
return value;
}
process('hello'); // ✅
process(42); // ✅
process(true); // ❌ Erro: boolean está fora do escopo da restrição
4. Common Interview Questions
Perguntas comuns em entrevistas
Pergunta 1: Implementar função genérica
Implemente uma função genérica first que retorne o primeiro elemento de um array.
function first<T>(arr: T[]): T | undefined {
// Sua implementação
}
Clique para ver a resposta
function first<T>(arr: T[]): T | undefined {
return arr.length > 0 ? arr[0] : undefined;
}
// Exemplo de uso
const firstString = first<string>(['a', 'b', 'c']); // 'a'
const firstNumber = first<number>([1, 2, 3]); // 1
const firstEmpty = first<number>([]); // undefined
Explicação:
<T>define o parâmetro de tipo genéricoarr: T[]representa um array do tipo T- O valor de retorno
T | undefinedindica que pode ser do tipo T ou undefined
Pergunta 2: Restrições genéricas
Implemente uma função que mescle dois objetos, mas apenas mesclando as propriedades existentes no primeiro objeto.
function merge<T, U>(obj1: T, obj2: U): T & U {
// Sua implementação
}
Clique para ver a resposta
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 } as T & U;
}
// Exemplo de uso
const obj1 = { name: 'John', age: 30 };
const obj2 = { age: 31, email: 'john@example.com' };
const merged = merge(obj1, obj2);
// { name: 'John', age: 31, email: 'john@example.com' }
Versão avançada (mesclar apenas propriedades do primeiro objeto):
function merge<T extends object, U extends Partial<T>>(
obj1: T,
obj2: U
): T {
return { ...obj1, ...obj2 };
}
const obj1 = { name: 'John', age: 30 };
const obj2 = { age: 31 }; // Só pode conter propriedades de obj1
const merged = merge(obj1, obj2);
// { name: 'John', age: 31 }
Pergunta 3: Interface genérica
Defina uma interface genérica Repository para operações de acesso a dados.
interface Repository<T> {
// Sua definição
}
Clique para ver a resposta
interface Repository<T> {
findById(id: string): T | undefined;
findAll(): T[];
save(entity: T): void;
delete(id: string): void;
}
// Exemplo de implementação
class UserRepository implements Repository<User> {
private users: User[] = [];
findById(id: string): User | undefined {
return this.users.find(user => user.id === id);
}
findAll(): User[] {
return this.users;
}
save(entity: User): void {
const index = this.users.findIndex(user => user.id === entity.id);
if (index >= 0) {
this.users[index] = entity;
} else {
this.users.push(entity);
}
}
delete(id: string): void {
this.users = this.users.filter(user => user.id !== id);
}
}
Pergunta 4: Restrições genéricas e keyof
Implemente uma função que obtenha o valor de uma propriedade de um objeto com base no nome da chave, garantindo a segurança de tipos.
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
// Sua implementação
}
Clique para ver a resposta
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Exemplo de uso
const user = {
name: 'John',
age: 30,
email: 'john@example.com',
};
const name = getValue(user, 'name'); // string
const age = getValue(user, 'age'); // number
const email = getValue(user, 'email'); // string
// const id = getValue(user, 'id'); // ❌ Erro: 'id' não é uma chave de user
Explicação:
K extends keyof Tgarante que K deve ser uma das chaves de TT[K]representa o tipo do valor correspondente à chave K no objeto T- Isso garante a segurança de tipos, permitindo descobrir erros em tempo de compilação
Pergunta 5: Tipos condicionais e genéricos
Explique os resultados da inferência de tipos do código a seguir.
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>;
type B = NonNullable<number | undefined>;
type C = NonNullable<string | number>;
Clique para ver a resposta
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>; // string
type B = NonNullable<number | undefined>; // number
type C = NonNullable<string | number>; // string | number
Explicação:
NonNullable<T>é um tipo condicional (Conditional Type)- Se T é atribuível a
null | undefined, retornanever; caso contrário, retornaT - Em
string | null,stringnão atende à condição enullatende, portanto o resultado éstring - Em
string | number, nenhum dos dois atende à condição, portanto o resultado éstring | number
Aplicação prática:
function processValue<T>(value: T): NonNullable<T> {
if (value === null || value === undefined) {
throw new Error('Value cannot be null or undefined');
}
return value as NonNullable<T>;
}
const result = processValue<string | null>('hello'); // string
5. Advanced Generic Patterns
Padrões avançados de genéricos
Parâmetros de tipo padrão
interface Container<T = string> {
value: T;
}
const container1: Container = { value: 'hello' }; // Usa o tipo padrão string
const container2: Container<number> = { value: 42 };
Múltiplos parâmetros de tipo
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
const numbers = [1, 2, 3];
const strings = map(numbers, (n) => n.toString()); // string[]
Tipos utilitários genéricos
// Partial: todas as propriedades se tornam opcionais
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Required: todas as propriedades se tornam obrigatórias
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Pick: selecionar propriedades específicas
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Omit: excluir propriedades específicas
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
6. Best Practices
Melhores práticas
Práticas recomendadas
// 1. Usar nomes de genéricos significativos
function process<TData, TResponse>(data: TData): TResponse {
// ...
}
// 2. Usar restrições para limitar o escopo dos genéricos
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
// 3. Fornecer parâmetros de tipo padrão
interface Config<T = string> {
value: T;
}
// 4. Usar tipos utilitários genéricos
type UserUpdate = Partial<User>;
type UserKeys = keyof User;
Práticas a evitar
// 1. Não abusar de genéricos
function process<T>(value: T): T { // ⚠️ Se há apenas um tipo, genéricos não são necessários
return value;
}
// 2. Não usar nomes de genéricos de uma única letra (exceto em casos simples)
function process<A, B, C>(a: A, b: B, c: C) { // ❌ Significado pouco claro
// ...
}
// 3. Não ignorar restrições
function process<T>(value: T) { // ⚠️ Se há limitações, restrições devem ser adicionadas
return value.length; // Possível erro
}
7. Interview Summary
Resumo para entrevistas
Referência rápida
Conceitos centrais de genéricos:
- Não especificar um tipo concreto ao definir, mas ao usar
- Sintaxe:
<T>define o parâmetro de tipo - Aplicável a funções, interfaces, classes
Restrições genéricas:
- Usar
extendspara limitar o escopo dos genéricos K extends keyof Tgarante que K é uma chave de T- Possível combinar múltiplas restrições
Padrões comuns:
- Função genérica:
function identity<T>(arg: T): T - Interface genérica:
interface Box<T> { value: T; } - Classe genérica:
class Container<T> { ... }
Exemplos de respostas para entrevistas
Q: O que são genéricos? Por que são necessários?
"Genéricos são um mecanismo no TypeScript para criar componentes reutilizáveis, onde o tipo concreto não é especificado na definição, mas no uso. As principais vantagens dos genéricos são: 1) Maior reutilização de código - uma função pode lidar com múltiplos tipos; 2) Manter a segurança de tipos - verificar erros de tipo em tempo de compilação; 3) Reduzir código duplicado - não é necessário escrever uma função para cada tipo. Por exemplo,
function identity<T>(arg: T): Tpode lidar com qualquer tipo sem precisar escrever funções separadas para string, number, etc."
Q: O que são restrições genéricas? Como usá-las?
"Restrições genéricas usam a palavra-chave
extendspara limitar o escopo do tipo genérico. Por exemplo,function getLength<T extends { length: number }>(arg: T)garante que T deve ter a propriedade length. Outra restrição comum éK extends keyof T, que garante que K deve ser uma das chaves de T, permitindo acesso a propriedades com segurança de tipos. Restrições ajudam a manter a segurança de tipos ao usar genéricos, fornecendo as informações de tipo necessárias."