<h2>Constraints: Limitando Tipos Generics</h2>
<p>Os constraints definem quais tipos podem ser passados como argumentos para um generic. Sem eles, você trabalha com qualquer tipo, perdendo a segurança do sistema de tipos. Um constraint é declarado com a palavra-chave <code>extends</code>.</p>
<p>Considere um cenário real: você precisa de uma função que retorne a propriedade <code>length</code> de um objeto. Nem todo tipo possui essa propriedade, então você limita o generic apenas a tipos que a possuem:</p>
<pre><code class="language-typescript">interface HasLength {
length: number;
}
function getLength<T extends HasLength>(item: T): number {
return item.length;
}
getLength("hello"); // ✓ string tem length
getLength([1, 2, 3]); // ✓ array tem length
getLength({ length: 5 }); // ✓ objeto com length funciona
// getLength(42); // ✗ Erro: number não tem length</code></pre>
<p>Você também pode constrainar usando tipos de propriedades específicas. Por exemplo, uma função que copia apenas propriedades de um objeto para outro:</p>
<pre><code class="language-typescript">function copyProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Ana", age: 30 };
const name = copyProperty(user, "name"); // ✓ type-safe
// copyProperty(user, "email"); // ✗ Erro: "email" não existe</code></pre>
<p>Constraints podem ser compostos usando intersecção. Imagine validar que um tipo estende múltiplas interfaces:</p>
<pre><code class="language-typescript">interface Named { name: string; }
interface Aged { age: number; }
function createPerson<T extends Named & Aged>(data: T): T {
return { ...data, updatedAt: new Date() };
}</code></pre>
<h3>Constraints com Tipos Primitivos</h3>
<p>Às vezes você quer garantir que um tipo genérico seja apenas string, number ou boolean. Use <code>extends</code> com literais:</p>
<pre><code class="language-typescript">function processValue<T extends string | number>(value: T): string {
return Processado: ${value};
}
processValue("texto"); // ✓
processValue(42); // ✓
// processValue(true); // ✗ Erro</code></pre>
<h2>Defaults: Valores Padrão para Generics</h2>
<p>Generics podem ter valores padrão, evitando que você sempre passe todos os argumentos de tipo. Isso melhora a ergonomia da API.</p>
<pre><code class="language-typescript">interface Repository<T = unknown> {
items: T[];
add(item: T): void;
getAll(): T[];
}
// Sem especificar tipo, T é unknown
const genericRepo: Repository = {
items: [],
add: (item) => {},
getAll: () => []
};
// Com tipo específico
interface User { id: number; name: string; }
const userRepo: Repository<User> = {
items: [],
add: (user) => {},
getAll: () => []
};</code></pre>
<p>Defaults são especialmente úteis em componentes React TypeScript. Imagine um componente de lista reutilizável:</p>
<pre><code class="language-typescript">interface ListProps<T = string, K extends keyof T = keyof T> {
items: T[];
keyExtractor: (item: T) => T[K];
renderItem: (item: T) => React.ReactNode;
}
// Funciona com tipo padrão
const stringList = (props: ListProps) => <div />;
// Ou com tipo customizado
interface Product { id: number; title: string; }
const productList = (props: ListProps<Product>) => <div />;</code></pre>
<p>Você pode combinar constraints com defaults. O tipo padrão deve satisfazer o constraint:</p>
<pre><code class="language-typescript">interface Config<T extends string | number = string> {
value: T;
validate(input: unknown): input is T;
}
const stringConfig: Config = {
value: "default",
validate: (x): x is string => typeof x === "string"
};
const numberConfig: Config<number> = {
value: 42,
validate: (x): x is number => typeof x === "number"
};</code></pre>
<h2>Variância: Covariância, Contravariância e Invariância</h2>
<p>Variância define como tipos genéricos se relacionam em hierarquias de herança. Este é o tema mais avançado e frequentemente mal compreendido.</p>
<h3>Covariância (out)</h3>
<p>Um tipo genérico é <strong>covariante</strong> quando você pode atribuir um subtipo onde um supertipo é esperado. Arrays TypeScript são covariantes:</p>
<pre><code class="language-typescript">class Animal { move() {} }
class Dog extends Animal { bark() {} }
const animals: Animal[] = [];
const dogs: Dog[] = [new Dog()];
// Covariância: Dog[] é atribuível a Animal[]
const list: Animal[] = dogs; // ✓ Funciona</code></pre>
<blockquote><p><strong>Cuidado</strong>: Isto cria um problema de segurança. Se você adicionar um <code>Cat</code> à lista, terá um <code>Cat</code> onde esperava um <code>Dog</code>.</p></blockquote>
<p>Generics custom são invariantes por padrão, mas você pode declarar covariância explicitamente com <code>out</code>:</p>
<pre><code class="language-typescript">interface Producer<out T> {
produce(): T;
}
class DogProducer implements Producer<Dog> {
produce(): Dog { return new Dog(); }
}
// Covariância: DogProducer pode ser atribuído a Producer<Animal>
const animalProducer: Producer<Animal> = new DogProducer(); // ✓
const animal = animalProducer.produce(); // type: Animal</code></pre>
<h3>Contravariância (in)</h3>
<p>Um tipo genérico é <strong>contravariante</strong> quando você pode usar um supertipo onde um subtipo é esperado. Funções de callback são contravariantes no tipo de parâmetro:</p>
<pre><code class="language-typescript">interface Consumer<in T> {
consume(item: T): void;
}
class AnimalConsumer implements Consumer<Animal> {
consume(animal: Animal) { console.log("Consumindo animal"); }
}
// Contravariância: AnimalConsumer pode ser atribuído a Consumer<Dog>
const dogConsumer: Consumer<Dog> = new AnimalConsumer(); // ✓
dogConsumer.consume(new Dog()); // Funciona porque Dog é Animal</code></pre>
<h3>Invariância</h3>
<p>Sem <code>in</code> ou <code>out</code>, o tipo é <strong>invariante</strong>: você não pode substituir nem por subtipos nem por supertipos. A maioria dos generics são invariantes:</p>
<pre><code class="language-typescript">interface Container<T> {
get(): T;
set(value: T): void;
}
const animalContainer: Container<Animal> = null!;
const dogContainer: Container<Dog> = null!;
// animalContainer = dogContainer; // ✗ Erro: Invariante
// dogContainer = animalContainer; // ✗ Erro: Invariante</code></pre>
<p>Invariância é mais segura porque impede leitura e escrita inseguras. Use <code>in</code> e <code>out</code> apenas quando apropriado.</p>
<h2>Conclusão</h2>
<p>Generics avançados em TypeScript capacitam você a escrever código type-safe e reutilizável em escala. <strong>Constraints limitam possibilidades</strong> mantendo flexibilidade, <strong>defaults reduzem boilerplate</strong> sem sacrificar clareza, e <strong>variância controla substituição de tipos</strong> em hierarquias complexas. Domine estes três pilares e você estará no nível sênior do sistema de tipos TypeScript. Na prática, comece com constraints, adicione defaults quando sentir dor, e explore variância apenas em APIs públicas avançadas.</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/types-from-types.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook - Advanced Types</a></li>
<li><a href="https://stackoverflow.com/questions/8481301/covariance-and-contravariance-not-just-raw-syntax-and-definition" target="_blank" rel="noopener noreferrer">Understanding TypeScript's Variance</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://effectivetypescript.com/" target="_blank" rel="noopener noreferrer">Effective TypeScript: 62 Specific Ways to Improve Your TypeScript</a></li>
</ul>