<h2>Entendendo Generics em TypeScript</h2>
<p>Os generics são um dos recursos mais poderosos do TypeScript, permitindo que você escreva código reutilizável e type-safe. Imagine que você precisa criar uma função que funcione com qualquer tipo de dado, mas sem perder a segurança de tipos que o TypeScript oferece. É exatamente isso que os generics fazem. Eles funcionam como variáveis de tipo, permitindo que você parametrize tipos da mesma forma que parametriza valores em funções.</p>
<p>A sintaxe básica usa a notação com colchetes angulares: <code><T></code>. Essa letra <code>T</code> é apenas uma convenção (de "Type"), mas você pode usar qualquer nome. Quando você define um generic, está dizendo: "eu vou trabalhar com algum tipo, mas você vai me dizer qual é no momento de usar". Isso oferece flexibilidade máxima sem sacrificar a segurança.</p>
<pre><code class="language-typescript">// Função genérica básica
function identity<T>(value: T): T {
return value;
}
// Usando a função
const numberResult = identity(42); // T é number
const stringResult = identity("hello"); // T é string
const boolResult = identity(true); // T é boolean
console.log(typeof numberResult); // "number"
console.log(typeof stringResult); // "string"</code></pre>
<p>O TypeScript infere o tipo automaticamente com base no argumento passado. Você também pode ser explícito se desejar: <code>identity<string>("hello")</code>. Ambas as abordagens funcionam, mas a inferência automática torna o código mais limpo.</p>
<h2>Constraints em Generics</h2>
<p>Constraints (restrições) são mecanismos que limitam quais tipos podem ser usados com um generic. Sem constraints, você poderia passar qualquer tipo, o que às vezes não faz sentido. Por exemplo, se você quer trabalhar apenas com objetos que têm uma propriedade específica, precisa de um constraint.</p>
<p>A sintaxe de constraint usa <code>extends</code>: <code><T extends AlgumaCoisa></code>. Isso significa que <code>T</code> pode ser qualquer tipo, desde que seja compatível com <code>AlgumaCoisa</code>. Os constraints tornam seu código mais seguro e mais expressivo sobre suas intenções.</p>
<pre><code class="language-typescript">// Constraint básico: T deve ter a propriedade 'length'
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
// Funciona com strings, arrays e objetos com length
console.log(getLength("hello")); // 5
console.log(getLength([1, 2, 3])); // 3
console.log(getLength({ length: 10 })); // 10
// Isso daria erro em tempo de compilação:
// getLength(42); // Error: Argument of type 'number' is not assignable</code></pre>
<h3>Constraints com Classes e Interfaces</h3>
<p>Você pode usar classes e interfaces como constraints também. Isso é especialmente útil quando você quer garantir que o tipo possui métodos específicos ou implementa uma interface.</p>
<pre><code class="language-typescript">interface HasId {
id: number;
}
function printId<T extends HasId>(obj: T): void {
console.log(ID: ${obj.id});
}
// Funciona
printId({ id: 1, name: "John" });
printId({ id: 2, email: "test@example.com" });
// Erro: 'id' não existe em number
// printId(123);</code></pre>
<h3>Constraints com Tipos Union e Keyof</h3>
<p>TypeScript permite constraints mais sofisticados usando tipos union e a palavra-chave <code>keyof</code>. O <code>keyof</code> extrai todas as chaves de um tipo, criando um union delas.</p>
<pre><code class="language-typescript">// Constraint: T deve ser uma string ou number
function processValue<T extends string | number>(value: T): string {
return Valor: ${value};
}
console.log(processValue("hello")); // OK
console.log(processValue(42)); // OK
// processValue(true); // Error
// Constraint 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 person = { name: "Alice", age: 30 };
const name = getProperty(person, "name"); // OK, retorna string
const age = getProperty(person, "age"); // OK, retorna number
// getProperty(person, "email"); // Error: "email" não é chave</code></pre>
<h2>Default Types em Generics</h2>
<p>Default types (tipos padrão) permitem que você especifique um tipo padrão quando nenhum é fornecido explicitamente. Isso é semelhante aos parâmetros padrão em funções, mas para tipos. Quando você não passa um tipo específico, o padrão é usado automaticamente.</p>
<p>A sintaxe é simples: <code><T = TipoPadrão></code>. Isso torna seus generics mais flexíveis, permitindo uso simples para casos comuns enquanto mantém a capacidade de especificar tipos diferentes quando necessário.</p>
<pre><code class="language-typescript">// Generic com tipo padrão
function create<T = string>(value?: T): T | undefined {
return value;
}
// Sem argumento de tipo, usa string como padrão
const result1 = create(); // T é string, retorna undefined
const result2 = create("hello"); // T é string
const result3 = create<number>(42); // T explicitamente number
const result4 = create<boolean>(true); // T explicitamente boolean
console.log(result2); // "hello"
console.log(result3); // 42</code></pre>
<h3>Default Types em Interfaces e Types</h3>
<p>Default types são especialmente úteis em interfaces e types, onde você define estruturas genéricas que serão reutilizadas em muitos lugares.</p>
<pre><code class="language-typescript">// Interface genérica com default type
interface ApiResponse<T = any> {
status: number;
data: T;
message: string;
}
// Usar sem especificar T
const response1: ApiResponse = {
status: 200,
data: "anything",
message: "Success"
};
// Usar com T específico
const response2: ApiResponse<{ id: number; name: string }> = {
status: 200,
data: { id: 1, name: "John" },
message: "Success"
};
// Type genérico com default
type Container<T = string> = {
value: T;
isEmpty: boolean;
};
const stringContainer: Container = { value: "hello", isEmpty: false };
const numberContainer: Container<number> = { value: 42, isEmpty: false };</code></pre>
<h3>Combinando Constraints e Defaults</h3>
<p>A combinação de constraints com default types é poderosa. O default deve ser compatível com o constraint, e o TypeScript validará isso.</p>
<pre><code class="language-typescript">// Generic com constraint e default type
interface Entity {
id: number;
}
function processEntity<T extends Entity = { id: number; name: string }>(
entity: T
): number {
return entity.id;
}
// Usando o default
const result1 = processEntity({ id: 1, name: "John" });
// Especificando um tipo diferente (mas ainda compatível com Entity)
const result2 = processEntity<{ id: number; email: string }>({
id: 2,
email: "test@example.com"
});
console.log(result1); // 1
console.log(result2); // 2</code></pre>
<h3>Default Types em Múltiplos Generics</h3>
<p>Quando você tem múltiplos parâmetros genéricos, cada um pode ter seu próprio default type. Isso oferece muita flexibilidade.</p>
<pre><code class="language-typescript">// Múltiplos generics com defaults
interface Paginated<T = any, P = number> {
items: T[];
pageNumber: P;
totalPages: P;
}
// Usar todos os defaults
const page1: Paginated = {
items: ["a", "b", "c"],
pageNumber: 1,
totalPages: 5
};
// Especificar apenas o primeiro
const page2: Paginated<{ id: number; title: string }> = {
items: [{ id: 1, title: "Post 1" }],
pageNumber: 1,
totalPages: 10
};
// Especificar ambos
const page3: Paginated<string, string> = {
items: ["item1", "item2"],
pageNumber: "first",
totalPages: "last"
};</code></pre>
<h2>Casos Práticos e Padrões Avançados</h2>
<p>Na prática profissional, constraints e defaults trabalham juntos para criar APIs robustas e amigáveis. Vamos ver alguns padrões que você encontrará em código real.</p>
<h3>Padrão: Generic Builders</h3>
<pre><code class="language-typescript">interface Builder<T> {
build(): T;
}
class ObjectBuilder<T extends Record<string, any> = { id: number }> {
private obj: Partial<T> = {};
set<K extends keyof T>(key: K, value: T[K]): this {
this.obj[key] = value;
return this;
}
build(): T {
return this.obj as T;
}
}
// Usar com tipo padrão
const builder1 = new ObjectBuilder()
.set("id", 1)
.build();
// Usar com tipo específico
interface User {
id: number;
name: string;
email: string;
}
const builder2 = new ObjectBuilder<User>()
.set("id", 1)
.set("name", "Alice")
.set("email", "alice@example.com")
.build();
console.log(builder2); // { id: 1, name: "Alice", email: "alice@example.com" }</code></pre>
<h3>Padrão: Generic Storage com Validação</h3>
<pre><code class="language-typescript">interface Validator<T> {
validate(value: unknown): value is T;
}
class TypedStorage<T, V extends Validator<T> = Validator<T>> {
private data: Map<string, T> = new Map();
constructor(private validator: V) {}
set(key: string, value: unknown): boolean {
if (this.validator.validate(value)) {
this.data.set(key, value);
return true;
}
return false;
}
get(key: string): T | undefined {
return this.data.get(key);
}
}
// Validator para números
const numberValidator: Validator<number> = {
validate: (value): value is number => typeof value === "number"
};
const numStorage = new TypedStorage(numberValidator);
numStorage.set("count", 42); // true
numStorage.set("count", "hello"); // false
console.log(numStorage.get("count")); // 42</code></pre>
<h3>Padrão: Generic Utilities com Keyof</h3>
<pre><code class="language-typescript">// Utilitário para filtrar e mapear objetos de forma type-safe
function pickProperties<T, K extends keyof T>(
obj: T,
...keys: K[]
): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => {
result[key] = obj[key];
});
return result;
}
interface Product {
id: number;
name: string;
price: number;
description: string;
}
const product: Product = {
id: 1,
name: "Laptop",
price: 999,
description: "High-performance laptop"
};
// Pegar apenas id e name
const summary = pickProperties(product, "id", "name");
console.log(summary); // { id: 1, name: "Laptop" }
// TypeScript garante que você só pode pedir chaves válidas:
// pickProperties(product, "invalid"); // Error</code></pre>
<h2>Conclusão</h2>
<p>Você aprendeu que <strong>constraints e defaults são ferramentas complementares</strong> que tornam seus generics mais seguros e úteis. Constraints garantem que você trabalhe apenas com tipos que fazem sentido para sua lógica, enquanto defaults oferecem convenção sensata para os casos mais comuns, reduzindo a necessidade de sempre especificar tipos explicitamente.</p>
<p>O segundo ponto importante é que <strong>a combinação desses dois recursos permite criar APIs genéricas que são simultaneamente flexíveis e seguras</strong>. Você consegue escrever código reutilizável que funciona com múltiplos tipos, mas sem perder a validação em tempo de compilação que torna o TypeScript valioso.</p>
<p>Por fim, entenda que <strong>esses padrões são encontrados em todas as bibliotecas TypeScript profissionais</strong>. Frameworks como NestJS, bibliotecas como Zod e padrões de design moderno usam extensivamente constraints e defaults. Dominando esses conceitos, você consegue não apenas usar essas ferramentas, mas também projetar suas próprias APIs genéricas de forma responsável.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://www.typescriptlang.org/docs/handbook/2/generics.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook: Generics</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints" target="_blank" rel="noopener noreferrer">TypeScript Handbook: Generic Constraints</a></li>
<li><a href="https://basarat.gitbook.io/typescript/type-system/generics" target="_blank" rel="noopener noreferrer">TypeScript Deep Dive - Generics</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html" target="_blank" rel="noopener noreferrer">Advanced TypeScript: Generic Constraints and Defaults</a></li>
<li><a href="https://www.oreilly.com/library/view/effective-typescript/9781492053736/" target="_blank" rel="noopener noreferrer">Effective TypeScript: 62 Specific Ways to Improve Your TypeScript - Dan Vanderkam</a></li>
</ul>
<p><!-- FIM --></p>