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.