Generics Introduzione

Edoardo Midali
Edoardo Midali

I generics in TypeScript permettono di scrivere codice riutilizzabile che opera su tipi diversi mantenendo type safety. Attraverso parametri tipo indicati con <T>, i generics definiscono placeholder per tipi che vengono specificati al momento dell’uso, consentendo di creare funzioni, classi e interfacce flessibili senza sacrificare le garanzie del type system.

Problema e Soluzione

Senza generics, è necessario duplicare codice per tipi diversi o perdere type safety usando any.

// Senza generics: duplicazione
function identitaNumero(valore: number): number {
  return valore;
}

function identitaStringa(valore: string): string {
  return valore;
}

// Senza generics: perdita type safety
function identitaAny(valore: any): any {
  return valore;
}

// Con generics: riutilizzabile e type-safe
function identita<T>(valore: T): T {
  return valore;
}

const num = identita(42); // number
const str = identita("hello"); // string
const bool = identita(true); // boolean

Sintassi Base

I generics si dichiarano con angle brackets <T> dove T è il parametro tipo convenzionale.

// Funzione generica
function primo<T>(array: T[]): T | undefined {
  return array[0];
}

// Arrow function
const ultimo = <T>(array: T[]): T | undefined => {
  return array[array.length - 1];
};

// Classe generica
class Box<T> {
  constructor(private contenuto: T) {}

  get(): T {
    return this.contenuto;
  }
}

const boxNum = new Box(42);
const boxStr = new Box("hello");

// Interfaccia generica
interface Coppia<T, U> {
  primo: T;
  secondo: U;
}

const coppia: Coppia<string, number> = {
  primo: "età",
  secondo: 30,
};

Inferenza dei Tipi

TypeScript inferisce automaticamente i tipi dai parametri quando possibile.

function coppia<T, U>(a: T, b: U): [T, U] {
  return [a, b];
}

// Tipo inferito automaticamente
const risultato = coppia("hello", 42); // [string, number]

// Tipo esplicito
const esplicito = coppia<string, number>("hello", 42);

Multipli Parametri Tipo

È possibile usare multipli parametri tipo per relazioni complesse.

function mappa<T, U>(array: T[], fn: (item: T) => U): U[] {
  return array.map(fn);
}

const numeri = [1, 2, 3];
const stringhe = mappa(numeri, (n) => n.toString()); // string[]

Constraints

I constraints limitano i tipi accettabili usando extends.

// Constraint semplice
function lunghezza<T extends { length: number }>(item: T): number {
  return item.length;
}

lunghezza("hello"); // OK
lunghezza([1, 2, 3]); // OK
// lunghezza(123);     // ERRORE

// Constraint con interface
interface Identificabile {
  id: number;
}

function getId<T extends Identificabile>(obj: T): number {
  return obj.id;
}

// Constraint con keyof
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const persona = { nome: "Mario", eta: 30 };
const nome = getProp(persona, "nome"); // string

Array e Collezioni

I generics sono fondamentali per array e collezioni type-safe.

// Array generici
const numeri: Array<number> = [1, 2, 3];
const stringhe: string[] = ["a", "b", "c"];

// Metodi array
const doppi = numeri.map((n) => n * 2); // number[]
const pari = numeri.filter((n) => n % 2 === 0); // number[]

// Collezione personalizzata
class Lista<T> {
  private items: T[] = [];

  aggiungi(item: T): void {
    this.items.push(item);
  }

  get(index: number): T | undefined {
    return this.items[index];
  }
}

Vantaggi dei Generics

I generics offrono benefici chiave rispetto ad alternative.

// Riutilizzo del codice
function wrapArray<T>(value: T): T[] {
  return [value];
}

wrapArray(1); // number[]
wrapArray("hello"); // string[]
wrapArray(true); // boolean[]

// Type safety preservata
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const merged = merge({ a: 1 }, { b: "hello" });
// { a: number } & { b: string }

// Documentazione implicita
function filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
  return array.filter(predicate);
}

Casi d’Uso Comuni

I generics risolvono pattern ricorrenti nella programmazione.

// Repository pattern
interface Repository<T> {
  find(id: number): T | undefined;
  save(item: T): void;
  delete(id: number): void;
}

// Response wrapper
interface Response<T> {
  success: boolean;
  data: T;
  error?: string;
}

// Promise e async
async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  return response.json();
}

// Utility functions
function clone<T>(obj: T): T {
  return JSON.parse(JSON.stringify(obj));
}

Nomenclatura Convenzionale

Convenzioni comuni per parametri tipo migliorano leggibilità.

// T: Type (generico)
function identity<T>(value: T): T {
  return value;
}

// K, V: Key, Value
interface Map<K, V> {
  get(key: K): V | undefined;
  set(key: K, value: V): void;
}

// E: Element
class Stack<E> {
  private items: E[] = [];
  push(item: E): void {
    this.items.push(item);
  }
}

// R: Return type
interface Transformer<T, R> {
  transform(input: T): R;
}

Quando Usare Generics

I generics sono appropriati per codice che opera su tipi multipli mantenendo relazioni di tipo.

// USARE generics: tipo di input determina output
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

// NON usare generics: input e output indipendenti
function getRandomNumber(): number {
  return Math.random();
}

// USARE generics: struttura dati riutilizzabile
class Queue<T> {
  private items: T[] = [];
  enqueue(item: T): void {
    this.items.push(item);
  }
  dequeue(): T | undefined {
    return this.items.shift();
  }
}

Conclusioni

I generics in TypeScript forniscono meccanismo essenziale per scrivere codice riutilizzabile e type-safe. Eliminando necessità di duplicazione o uso di any, i generics permettono di creare funzioni, classi e interfacce che operano su tipi diversi mantenendo garanzie complete del type system. Comprendere quando e come usare generics risulta fondamentale per sfruttare appieno la potenza di TypeScript, abilitando astrazione elegante senza sacrificare sicurezza dei tipi.