Generics Funzioni

Edoardo Midali
Edoardo Midali

Le funzioni generiche in TypeScript permettono di scrivere codice riutilizzabile che opera su tipi diversi mantenendo type safety completa. Dichiarate con parametri tipo <T>, le funzioni generiche definiscono placeholder per tipi che vengono inferiti o specificati alla chiamata, consentendo di creare utility functions flessibili che preservano informazioni di tipo attraverso trasformazioni e operazioni.

Fondamenti e Sintassi

Una funzione generica si dichiara aggiungendo parametri tipo tra angle brackets prima della lista parametri.

// Funzione generica base
function identita<T>(valore: T): T {
  return valore;
}

// Chiamata con tipo esplicito
const num = identita<number>(42);
const str = identita<string>("hello");

// Chiamata con inferenza automatica
const bool = identita(true); // T inferito come boolean
const arr = identita([1, 2, 3]); // T inferito come number[]

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

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

Inferenza del Tipo

TypeScript inferisce automaticamente i parametri tipo dagli argomenti della funzione, eliminando necessità di specificarli esplicitamente nella maggior parte dei casi.

// Inferenza da argomenti
function coppia<T, U>(primo: T, secondo: U): [T, U] {
  return [primo, secondo];
}

const risultato = coppia("hello", 42); // [string, number]
const altro = coppia(true, [1, 2]); // [boolean, number[]]

// Inferenza da contesto return
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[]
const doppi = mappa(numeri, (n) => n * 2); // number[]

// Inferenza parziale
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

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

Multipli Parametri Tipo

Le funzioni possono avere multipli parametri tipo per relazioni complesse tra argomenti e return.

// Due parametri tipo
function zip<T, U>(arr1: T[], arr2: U[]): Array<[T, U]> {
  const length = Math.min(arr1.length, arr2.length);
  const result: Array<[T, U]> = [];

  for (let i = 0; i < length; i++) {
    result.push([arr1[i], arr2[i]]);
  }

  return result;
}

const zipped = zip([1, 2, 3], ["a", "b", "c"]);
// Array<[number, string]>

// Tre parametri tipo
function combina<T, U, V>(a: T, b: U, fn: (x: T, y: U) => V): V {
  return fn(a, b);
}

const risultato = combina(5, "hello", (n, s) => s.repeat(n));
// string

// Parametri tipo correlati
function filtraEMappa<T, U>(
  array: T[],
  predicate: (item: T) => boolean,
  transform: (item: T) => U
): U[] {
  return array.filter(predicate).map(transform);
}

Constraints sui Parametri Tipo

Applicare vincoli ai parametri tipo per limitare i tipi accettabili e accedere a proprietà specifiche.

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

lunghezza("hello"); // OK: string ha length
lunghezza([1, 2, 3]); // OK: array ha length
// lunghezza(123);      // ERRORE: number non ha length

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

function trovaId<T extends Identificabile>(
  items: T[],
  id: number
): T | undefined {
  return items.find((item) => item.id === id);
}

interface Utente {
  id: number;
  nome: string;
}

const utenti: Utente[] = [
  { id: 1, nome: "Mario" },
  { id: 2, nome: "Anna" },
];

const trovato = trovaId(utenti, 1); // Utente | undefined

// Constraint con union
function processa<T extends string | number>(valore: T): string {
  if (typeof valore === "string") {
    return valore.toUpperCase();
  }
  return valore.toFixed(2);
}

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

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

Generic con Default Types

Specificare tipi default per parametri tipo quando non vengono forniti.

// Default type parameter
function creaArray<T = string>(lunghezza: number, valore: T): T[] {
  return Array(lunghezza).fill(valore);
}

const stringhe = creaArray(3, ""); // T = string (default)
const numeri = creaArray<number>(3, 0); // T = number (esplicito)

// Multipli default
function converti<Input = string, Output = number>(
  valore: Input,
  converter: (val: Input) => Output
): Output {
  return converter(valore);
}

const num = converti("123", parseInt); // Input=string, Output=number

Conditional Return Types

Return types che dipendono condizionalmente dai tipi di input.

// Conditional type
type ArrayOrSingle<T, IsArray extends boolean> = IsArray extends true ? T[] : T;

function elabora<T, IsArray extends boolean = false>(
  valore: T,
  asArray?: IsArray
): ArrayOrSingle<T, IsArray> {
  return (asArray ? [valore] : valore) as ArrayOrSingle<T, IsArray>;
}

const single = elabora(42); // number
const array = elabora(42, true); // number[]

// Overload per conditional behavior
function get<T>(array: T[], index: number): T | undefined;
function get<T>(array: T[]): T[];
function get<T>(array: T[], index?: number): T | T[] | undefined {
  if (index !== undefined) {
    return array[index];
  }
  return array;
}

Funzioni Higher-Order Generiche

Funzioni che accettano o restituiscono altre funzioni generiche.

// Funzione che restituisce funzione generica
function creaValidatore<T>(
  validazione: (valore: T) => boolean
): (valore: T) => boolean {
  return (valore: T) => validazione(valore);
}

const validaPositivo = creaValidatore((n: number) => n > 0);
const validaNonVuoto = creaValidatore((s: string) => s.length > 0);

// Compose generico
function componi<T>(f: (x: T) => T, g: (x: T) => T): (x: T) => T {
  return (x: T) => f(g(x));
}

const aggiungi1 = (n: number) => n + 1;
const raddoppia = (n: number) => n * 2;
const trasforma = componi(aggiungi1, raddoppia);

console.log(trasforma(5)); // 11

// Pipe generico
function pipe<T>(...funzioni: Array<(x: T) => T>): (x: T) => T {
  return (valore: T) => {
    return funzioni.reduce((acc, fn) => fn(acc), valore);
  };
}

const elabora = pipe(
  (n: number) => n + 1,
  (n: number) => n * 2,
  (n: number) => n ** 2
);

// Curry generico
function curry<T, U, V>(fn: (a: T, b: U) => V): (a: T) => (b: U) => V {
  return (a: T) => (b: U) => fn(a, b);
}

const somma = (a: number, b: number) => a + b;
const sommaCurried = curry(somma);
const aggiungi10 = sommaCurried(10);
console.log(aggiungi10(5)); // 15

Generic con Rest Parameters

Combinare generics con rest parameters per funzioni variadic type-safe.

// Rest parameters con generic
function sommaMultipla<T extends number>(...numeri: T[]): number {
  return numeri.reduce((acc, n) => acc + n, 0);
}

console.log(sommaMultipla(1, 2, 3, 4, 5)); // 15

// Tuple variadic
function concatenaTuple<T extends any[]>(...args: T): string {
  return args.join(", ");
}

const risultato = concatenaTuple("a", 1, true); // "a, 1, true"

// Generic con spread
function merge<T extends object[]>(...oggetti: T): object {
  return Object.assign({}, ...oggetti);
}

const merged = merge({ a: 1 }, { b: 2 }, { c: 3 });

Type Guards e Narrowing

Funzioni generiche che agiscono come type guards per narrowing.

// Type predicate
function isString(valore: unknown): valore is string {
  return typeof valore === "string";
}

function isNumber(valore: unknown): valore is number {
  return typeof valore === "number";
}

// Generic type guard
function isArrayOf<T>(
  valore: unknown,
  guard: (item: unknown) => item is T
): valore is T[] {
  return Array.isArray(valore) && valore.every(guard);
}

const mixed: unknown = [1, 2, 3];

if (isArrayOf(mixed, isNumber)) {
  // mixed è number[] qui
  const somma = mixed.reduce((a, b) => a + b, 0);
}

// Type guard con constraint
function hasProprietà<T, K extends string>(
  obj: T,
  key: K
): obj is T & Record<K, unknown> {
  return typeof obj === "object" && obj !== null && key in obj;
}

const obj: unknown = { nome: "Mario" };

if (hasProprietà(obj, "nome")) {
  console.log(obj.nome); // OK: obj ha proprietà nome
}

Assertion Signatures

Funzioni che assicurano tipi attraverso asserzioni.

// Assert signature
function assertIsString(valore: unknown): asserts valore is string {
  if (typeof valore !== "string") {
    throw new Error("Non è una stringa");
  }
}

function processa(input: unknown): void {
  assertIsString(input);
  // Dopo assert, input è string
  console.log(input.toUpperCase());
}

// Generic assert
function assertType<T>(
  valore: unknown,
  guard: (val: unknown) => val is T
): asserts valore is T {
  if (!guard(valore)) {
    throw new Error("Type assertion fallita");
  }
}

function assertIsNumber(val: unknown): val is number {
  return typeof val === "number";
}

const value: unknown = 42;
assertType(value, assertIsNumber);
// value è number qui
console.log(value * 2);

Funzioni Asincrone Generiche

Generics con Promise e async/await per operazioni asincrone type-safe.

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

interface Utente {
  id: number;
  nome: string;
}

const utente = await fetchData<Utente>("/api/utenti/1");

// Generic con Promise.all
async function fetchMultipli<T>(urls: string[]): Promise<T[]> {
  const promises = urls.map((url) => fetchData<T>(url));
  return await Promise.all(promises);
}

// Retry generico
async function conRetry<T>(
  fn: () => Promise<T>,
  maxTentativi: number = 3
): Promise<T> {
  for (let i = 0; i < maxTentativi; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxTentativi - 1) throw error;
      await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
    }
  }
  throw new Error("Max tentativi raggiunti");
}

// Timeout generico
async function conTimeout<T>(
  promise: Promise<T>,
  timeoutMs: number
): Promise<T> {
  return Promise.race([
    promise,
    new Promise<T>((_, reject) =>
      setTimeout(() => reject(new Error("Timeout")), timeoutMs)
    ),
  ]);
}

Utility Functions Generiche

Pattern comuni implementati con funzioni generiche riutilizzabili.

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

// Pick properties
function pick<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;
}

const persona = { nome: "Mario", eta: 30, citta: "Roma" };
const subset = pick(persona, "nome", "eta"); // { nome: string; eta: number }

// Omit properties
function omit<T, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> {
  const result = { ...obj };
  keys.forEach((key) => delete result[key]);
  return result;
}

// GroupBy
function groupBy<T, K extends string | number>(
  array: T[],
  keyFn: (item: T) => K
): Record<K, T[]> {
  return array.reduce((groups, item) => {
    const key = keyFn(item);
    if (!groups[key]) {
      groups[key] = [];
    }
    groups[key].push(item);
    return groups;
  }, {} as Record<K, T[]>);
}

// Unique
function unique<T>(array: T[]): T[] {
  return [...new Set(array)];
}

// Chunk
function chunk<T>(array: T[], size: number): T[][] {
  const chunks: T[][] = [];
  for (let i = 0; i < array.length; i += size) {
    chunks.push(array.slice(i, i + size));
  }
  return chunks;
}

Memoization Generica

Implementare memoization con funzioni generiche per caching.

// Memoize function
function memoize<T extends any[], R>(fn: (...args: T) => R): (...args: T) => R {
  const cache = new Map<string, R>();

  return (...args: T): R => {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key)!;
    }

    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const fibonacci = memoize((n: number): number => {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(40)); // Veloce grazie alla cache

// Memoize async
function memoizeAsync<T extends any[], R>(
  fn: (...args: T) => Promise<R>
): (...args: T) => Promise<R> {
  const cache = new Map<string, Promise<R>>();

  return async (...args: T): Promise<R> => {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key)!;
    }

    const promise = fn(...args);
    cache.set(key, promise);
    return promise;
  };
}

Conclusioni

Le funzioni generiche in TypeScript forniscono meccanismo potente per scrivere codice riutilizzabile e type-safe che opera su tipi diversi. Combinando parametri tipo, constraints, inferenza automatica, e features avanzate come conditional types e assertion signatures, è possibile creare utility functions flessibili che preservano informazioni di tipo attraverso trasformazioni complesse. Generics in funzioni eliminano duplicazione, migliorano manutenibilità, e permettono al compilatore di verificare correttezza delle operazioni, risultando essenziali per librerie, framework, e applicazioni che richiedono astrazione su tipi diversi mantenendo garanzie statiche complete.