JavaScript Avançado

Guia Completo de Generics Avançados em TypeScript: Constraints, Defaults e Variância

8 min de leitura

Guia Completo de Generics Avançados em TypeScript: Constraints, Defaults e Variância

Constraints: Limitando Tipos Generics 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 . Considere um cenário real: você precisa de uma função que retorne a propriedade de um objeto. Nem todo tipo possui essa propriedade, então você limita o generic apenas a tipos que a possuem: 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: Constraints podem ser compostos usando intersecção. Imagine validar que um tipo estende múltiplas interfaces: Constraints com Tipos Primitivos Às vezes você quer garantir que um tipo genérico seja apenas string, number ou boolean. Use com literais: Processado: ${value} Defaults: Valores Padrão para Generics Generics podem ter valores padrão, evitando que você sempre passe todos os argumentos de tipo. Isso melhora a ergonomia da API. Defaults são

<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&lt;T extends HasLength&gt;(item: T): number {

return item.length;

}

getLength(&quot;hello&quot;); // ✓ 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&lt;T, K extends keyof T&gt;(obj: T, key: K): T[K] {

return obj[key];

}

const user = { name: &quot;Ana&quot;, age: 30 };

const name = copyProperty(user, &quot;name&quot;); // ✓ type-safe

// copyProperty(user, &quot;email&quot;); // ✗ Erro: &quot;email&quot; 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&lt;T extends Named &amp; Aged&gt;(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&lt;T extends string | number&gt;(value: T): string {

return Processado: ${value};

}

processValue(&quot;texto&quot;); // ✓

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&lt;T = unknown&gt; {

items: T[];

add(item: T): void;

getAll(): T[];

}

// Sem especificar tipo, T é unknown

const genericRepo: Repository = {

items: [],

add: (item) =&gt; {},

getAll: () =&gt; []

};

// Com tipo específico

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

const userRepo: Repository&lt;User&gt; = {

items: [],

add: (user) =&gt; {},

getAll: () =&gt; []

};</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&lt;T = string, K extends keyof T = keyof T&gt; {

items: T[];

keyExtractor: (item: T) =&gt; T[K];

renderItem: (item: T) =&gt; React.ReactNode;

}

// Funciona com tipo padrão

const stringList = (props: ListProps) =&gt; &lt;div /&gt;;

// Ou com tipo customizado

interface Product { id: number; title: string; }

const productList = (props: ListProps&lt;Product&gt;) =&gt; &lt;div /&gt;;</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&lt;T extends string | number = string&gt; {

value: T;

validate(input: unknown): input is T;

}

const stringConfig: Config = {

value: &quot;default&quot;,

validate: (x): x is string =&gt; typeof x === &quot;string&quot;

};

const numberConfig: Config&lt;number&gt; = {

value: 42,

validate: (x): x is number =&gt; typeof x === &quot;number&quot;

};</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&lt;out T&gt; {

produce(): T;

}

class DogProducer implements Producer&lt;Dog&gt; {

produce(): Dog { return new Dog(); }

}

// Covariância: DogProducer pode ser atribuído a Producer&lt;Animal&gt;

const animalProducer: Producer&lt;Animal&gt; = 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&lt;in T&gt; {

consume(item: T): void;

}

class AnimalConsumer implements Consumer&lt;Animal&gt; {

consume(animal: Animal) { console.log(&quot;Consumindo animal&quot;); }

}

// Contravariância: AnimalConsumer pode ser atribuído a Consumer&lt;Dog&gt;

const dogConsumer: Consumer&lt;Dog&gt; = 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&lt;T&gt; {

get(): T;

set(value: T): void;

}

const animalContainer: Container&lt;Animal&gt; = null!;

const dogContainer: Container&lt;Dog&gt; = 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&#039;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>

Comentários

Mais em JavaScript Avançado

Dominando Fastify em Node.js: Alta Performance e Schema Validation com JSON Schema em Projetos Reais
Dominando Fastify em Node.js: Alta Performance e Schema Validation com JSON Schema em Projetos Reais

Por que Fastify? Fastify é um framework web moderno para Node.js que se desta...

Guia Completo de Next.js Avançado: SSR, SSG, ISR e App Router com Server Components
Guia Completo de Next.js Avançado: SSR, SSG, ISR e App Router com Server Components

Renderização no Next.js: Entendendo SSR, SSG e ISR A escolha da estratégia de...

Boas Práticas de PostgreSQL Avançado com Node.js: Transactions, CTEs e Window Functions para Times Ágeis
Boas Práticas de PostgreSQL Avançado com Node.js: Transactions, CTEs e Window Functions para Times Ágeis

Transactions com PostgreSQL e Node.js As transações são fundamentais para gar...