È uscito il Corso Java Completo — usa il coupon JAVA2026 (fino al 30 giugno)

Generics Funzioni

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.