TypeScript

Como Usar Constraints e Default Types em Generics TypeScript em Produção

12 min de leitura

Como Usar Constraints e Default Types em Generics TypeScript em Produção

Entendendo Generics em TypeScript 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. A sintaxe básica usa a notação com colchetes angulares: . Essa letra é 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. O TypeScript infere o tipo automaticamente com base no argumento passado. Você também pode ser explícito se desejar: . Ambas as abordagens funcionam, mas a inferência automática torna o código mais

<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>&lt;T&gt;</code>. Essa letra <code>T</code> é apenas uma convenção (de &quot;Type&quot;), mas você pode usar qualquer nome. Quando você define um generic, está dizendo: &quot;eu vou trabalhar com algum tipo, mas você vai me dizer qual é no momento de usar&quot;. Isso oferece flexibilidade máxima sem sacrificar a segurança.</p>

<pre><code class="language-typescript">// Função genérica básica

function identity&lt;T&gt;(value: T): T {

return value;

}

// Usando a função

const numberResult = identity(42); // T é number

const stringResult = identity(&quot;hello&quot;); // T é string

const boolResult = identity(true); // T é boolean

console.log(typeof numberResult); // &quot;number&quot;

console.log(typeof stringResult); // &quot;string&quot;</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&lt;string&gt;(&quot;hello&quot;)</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>&lt;T extends AlgumaCoisa&gt;</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 &#039;length&#039;

function getLength&lt;T extends { length: number }&gt;(value: T): number {

return value.length;

}

// Funciona com strings, arrays e objetos com length

console.log(getLength(&quot;hello&quot;)); // 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 &#039;number&#039; 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&lt;T extends HasId&gt;(obj: T): void {

console.log(ID: ${obj.id});

}

// Funciona

printId({ id: 1, name: &quot;John&quot; });

printId({ id: 2, email: &quot;test@example.com&quot; });

// Erro: &#039;id&#039; 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&lt;T extends string | number&gt;(value: T): string {

return Valor: ${value};

}

console.log(processValue(&quot;hello&quot;)); // OK

console.log(processValue(42)); // OK

// processValue(true); // Error

// Constraint com keyof: K deve ser uma chave de T

function getProperty&lt;T, K extends keyof T&gt;(obj: T, key: K): T[K] {

return obj[key];

}

const person = { name: &quot;Alice&quot;, age: 30 };

const name = getProperty(person, &quot;name&quot;); // OK, retorna string

const age = getProperty(person, &quot;age&quot;); // OK, retorna number

// getProperty(person, &quot;email&quot;); // Error: &quot;email&quot; 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>&lt;T = TipoPadrão&gt;</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&lt;T = string&gt;(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(&quot;hello&quot;); // T é string

const result3 = create&lt;number&gt;(42); // T explicitamente number

const result4 = create&lt;boolean&gt;(true); // T explicitamente boolean

console.log(result2); // &quot;hello&quot;

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

status: number;

data: T;

message: string;

}

// Usar sem especificar T

const response1: ApiResponse = {

status: 200,

data: &quot;anything&quot;,

message: &quot;Success&quot;

};

// Usar com T específico

const response2: ApiResponse&lt;{ id: number; name: string }&gt; = {

status: 200,

data: { id: 1, name: &quot;John&quot; },

message: &quot;Success&quot;

};

// Type genérico com default

type Container&lt;T = string&gt; = {

value: T;

isEmpty: boolean;

};

const stringContainer: Container = { value: &quot;hello&quot;, isEmpty: false };

const numberContainer: Container&lt;number&gt; = { 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&lt;T extends Entity = { id: number; name: string }&gt;(

entity: T

): number {

return entity.id;

}

// Usando o default

const result1 = processEntity({ id: 1, name: &quot;John&quot; });

// Especificando um tipo diferente (mas ainda compatível com Entity)

const result2 = processEntity&lt;{ id: number; email: string }&gt;({

id: 2,

email: &quot;test@example.com&quot;

});

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&lt;T = any, P = number&gt; {

items: T[];

pageNumber: P;

totalPages: P;

}

// Usar todos os defaults

const page1: Paginated = {

items: [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;],

pageNumber: 1,

totalPages: 5

};

// Especificar apenas o primeiro

const page2: Paginated&lt;{ id: number; title: string }&gt; = {

items: [{ id: 1, title: &quot;Post 1&quot; }],

pageNumber: 1,

totalPages: 10

};

// Especificar ambos

const page3: Paginated&lt;string, string&gt; = {

items: [&quot;item1&quot;, &quot;item2&quot;],

pageNumber: &quot;first&quot;,

totalPages: &quot;last&quot;

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

build(): T;

}

class ObjectBuilder&lt;T extends Record&lt;string, any&gt; = { id: number }&gt; {

private obj: Partial&lt;T&gt; = {};

set&lt;K extends keyof T&gt;(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(&quot;id&quot;, 1)

.build();

// Usar com tipo específico

interface User {

id: number;

name: string;

email: string;

}

const builder2 = new ObjectBuilder&lt;User&gt;()

.set(&quot;id&quot;, 1)

.set(&quot;name&quot;, &quot;Alice&quot;)

.set(&quot;email&quot;, &quot;alice@example.com&quot;)

.build();

console.log(builder2); // { id: 1, name: &quot;Alice&quot;, email: &quot;alice@example.com&quot; }</code></pre>

<h3>Padrão: Generic Storage com Validação</h3>

<pre><code class="language-typescript">interface Validator&lt;T&gt; {

validate(value: unknown): value is T;

}

class TypedStorage&lt;T, V extends Validator&lt;T&gt; = Validator&lt;T&gt;&gt; {

private data: Map&lt;string, T&gt; = 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&lt;number&gt; = {

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

};

const numStorage = new TypedStorage(numberValidator);

numStorage.set(&quot;count&quot;, 42); // true

numStorage.set(&quot;count&quot;, &quot;hello&quot;); // false

console.log(numStorage.get(&quot;count&quot;)); // 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&lt;T, K extends keyof T&gt;(

obj: T,

...keys: K[]

): Pick&lt;T, K&gt; {

const result = {} as Pick&lt;T, K&gt;;

keys.forEach(key =&gt; {

result[key] = obj[key];

});

return result;

}

interface Product {

id: number;

name: string;

price: number;

description: string;

}

const product: Product = {

id: 1,

name: &quot;Laptop&quot;,

price: 999,

description: &quot;High-performance laptop&quot;

};

// Pegar apenas id e name

const summary = pickProperties(product, &quot;id&quot;, &quot;name&quot;);

console.log(summary); // { id: 1, name: &quot;Laptop&quot; }

// TypeScript garante que você só pode pedir chaves válidas:

// pickProperties(product, &quot;invalid&quot;); // 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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Guia Completo de Formulários com TypeScript: React Hook Form e Zod Integrados
Guia Completo de Formulários com TypeScript: React Hook Form e Zod Integrados

Introdução: Por que React Hook Form com Zod? Trabalhar com formulários em Rea...

Guia Completo de Recursive Types em TypeScript: Árvores, JSON e Estruturas Profundas
Guia Completo de Recursive Types em TypeScript: Árvores, JSON e Estruturas Profundas

Entendendo Tipos Recursivos em TypeScript Um tipo recursivo é aquele que faz...

Como Usar tRPC: APIs End-to-End Tipadas sem Geração de Código em Produção
Como Usar tRPC: APIs End-to-End Tipadas sem Geração de Código em Produção

O Problema das APIs Tradicionais Quando desenvolvemos aplicações modernas com...