Funzioni Arrow

Edoardo Midali
Edoardo Midali

Le funzioni arrow, introdotte in ES6 e pienamente supportate in TypeScript, rappresentano una sintassi concisa per definire funzioni con caratteristiche specifiche nel binding di this e nella sintassi di ritorno. L’operatore => (fat arrow) permette di scrivere funzioni più brevi ed espressive, particolarmente utili per callback e programmazione funzionale.

Fondamenti e Sintassi

La sintassi base utilizza l’operatore => per separare parametri dal corpo della funzione. Le parentesi dei parametri sono opzionali per singoli parametri, mentre il corpo può essere un’espressione o un blocco di statement.

// Sintassi completa
const somma = (a: number, b: number): number => {
  return a + b;
};

// Sintassi concisa con ritorno implicito
const doppio = (n: number) => n * 2;

// Parametro singolo senza parentesi
const quadrato = (n) => n * n;

// Senza parametri
const saluta = () => console.log("Ciao");

// Multipli parametri richiedono parentesi
const moltiplica = (a: number, b: number) => a * b;

Ritorno Implicito

Quando il corpo è una singola espressione, l’arrow function restituisce automaticamente il valore senza keyword return. Per restituire oggetti letterali, è necessario avvolgerli in parentesi.

// Ritorno implicito di valore
const triplo = (n: number) => n * 3;

// Ritorno implicito di oggetto (parentesi necessarie)
const creaPersona = (nome: string, eta: number) => ({
  nome,
  eta,
  maggiorenne: eta >= 18,
});

// Senza parentesi: sintassi ambigua interpretata come blocco
const errore = (nome: string) => {
  nome: nome;
}; // Restituisce undefined!

// Array con ritorno implicito
const numeri = [1, 2, 3].map((n) => n * 2);

Binding Lessicale di This

La differenza principale rispetto a function tradizionali è che arrow function non hanno proprio this, ma catturano this dal contesto lessicale circostante. Questo elimina problemi comuni con perdita di contesto.

class Timer {
  secondi = 0;

  // Arrow function: this è sempre Timer
  avvia() {
    setInterval(() => {
      this.secondi++;
      console.log(this.secondi);
    }, 1000);
  }

  // Function tradizionale: this si perde
  avviaProblematico() {
    setInterval(function () {
      this.secondi++; // Errore: this è undefined
    }, 1000);
  }
}

// Event handlers
class Bottone {
  conteggio = 0;

  // Arrow mantiene this della classe
  onClick = () => {
    this.conteggio++;
    console.log(`Cliccato ${this.conteggio} volte`);
  };
}

const btn = new Bottone();
document.addEventListener("click", btn.onClick); // Funziona

Tipizzazione dei Parametri

TypeScript richiede tipi espliciti per parametri quando non possono essere inferiti dal contesto. Il tipo di ritorno è spesso inferito automaticamente.

// Tipi espliciti
const somma = (a: number, b: number): number => a + b;

// Tipo ritorno inferito
const concatena = (a: string, b: string) => a + b; // string inferito

// Parametri con valori default
const saluta = (nome: string = "Guest") => `Ciao ${nome}`;

// Rest parameters
const sommaMultipla = (...numeri: number[]) => {
  return numeri.reduce((acc, n) => acc + n, 0);
};

// Destructuring nei parametri
const descriviPersona = ({ nome, eta }: { nome: string; eta: number }) => {
  return `${nome}, ${eta} anni`;
};

Type Annotations per Arrow Function

È possibile definire il tipo completo di una arrow function, separando la dichiarazione dalla implementazione.

// Tipo funzione completo
type OperazioneBinaria = (a: number, b: number) => number;

const somma: OperazioneBinaria = (a, b) => a + b;
const moltiplica: OperazioneBinaria = (a, b) => a * b;

// Tipo con generics
type Trasformatore<T, U> = (input: T) => U;

const lunghezza: Trasformatore<string, number> = (str) => str.length;
const maiuscolo: Trasformatore<string, string> = (str) => str.toUpperCase();

// Interface per tipo funzione
interface Validatore {
  (valore: string): boolean;
}

const nonVuoto: Validatore = (str) => str.length > 0;

Arrow Function come Metodi

Arrow function usate come metodi di classe hanno comportamento speciale: sono proprietà dell’istanza, non del prototipo, garantendo binding corretto di this ma aumentando memoria per istanza.

class Contatore {
  valore = 0;

  // Arrow come proprietà: binding this garantito
  incrementa = () => {
    this.valore++;
  };

  // Metodo tradizionale: sul prototipo
  decrementa() {
    this.valore--;
  }
}

const c = new Contatore();
const inc = c.incrementa;
inc(); // Funziona: this è sempre Contatore

const dec = c.decrementa;
dec(); // Errore: this è undefined

Callback e Higher-Order Functions

Arrow function eccellono come callback grazie a sintassi concisa e binding this prevedibile.

// Array methods
const numeri = [1, 2, 3, 4, 5];

const pari = numeri.filter((n) => n % 2 === 0);
const doppi = numeri.map((n) => n * 2);
const somma = numeri.reduce((acc, n) => acc + n, 0);

// Chaining
const risultato = [1, 2, 3, 4, 5]
  .filter((n) => n > 2)
  .map((n) => n * 2)
  .reduce((acc, n) => acc + n, 0);

// Callback personalizzate
function ripeti(n: number, azione: () => void) {
  for (let i = 0; i < n; i++) {
    azione();
  }
}

ripeti(3, () => console.log("Ciao"));

Arrow Function Asincrone

Arrow function supportano async/await, permettendo gestione concisa di operazioni asincrone.

// Async arrow function
const fetchUtente = async (id: string) => {
  const response = await fetch(`/api/utenti/${id}`);
  return await response.json();
};

// Con gestione errori
const caricaDati = async (id: string) => {
  try {
    const dati = await fetchUtente(id);
    return dati;
  } catch (error) {
    console.error(error);
    return null;
  }
};

// Array di Promise
const ids = ["1", "2", "3"];
const promises = ids.map(async (id) => await fetchUtente(id));
const risultati = await Promise.all(promises);

Limitazioni delle Arrow Function

Le arrow function non hanno arguments, super, o new.target, e non possono essere usate come costruttori. Queste limitazioni sono intenzionali per mantenere semplicità semantica.

// Non hanno arguments
const tradizionale = function () {
  console.log(arguments); // OK
};

const arrow = () => {
  console.log(arguments); // Errore
};

// Alternativa con rest parameters
const arrowConRest = (...args: any[]) => {
  console.log(args); // OK
};

// Non possono essere costruttori
const Costruttore = (nome: string) => {
  this.nome = nome; // Errore
};

// new Costruttore("test"); // Errore: Arrow function non può essere chiamata con new

Currying e Composizione

Arrow function rendono elegante currying e composizione di funzioni grazie alla sintassi concisa.

// Currying
const somma = (a: number) => (b: number) => a + b;
const aggiungi5 = somma(5);
console.log(aggiungi5(3)); // 8

// Currying con tipi
const moltiplica = (a: number) => (b: number) => (c: number) => a * b * c;
console.log(moltiplica(2)(3)(4)); // 24

// Composizione
const componi =
  <T>(f: (x: T) => T, g: (x: T) => T) =>
  (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

Arrow Function in Contesti Diversi

Arrow function si adattano a vari pattern e contesti, da proprietà di oggetti a elementi di array.

// Oggetto con arrow function
const calcolatrice = {
  somma: (a: number, b: number) => a + b,
  sottrai: (a: number, b: number) => a - b,
};

// Array di funzioni
const operazioni = [
  (n: number) => n + 1,
  (n: number) => n * 2,
  (n: number) => n ** 2,
];

let valore = 3;
operazioni.forEach((op) => (valore = op(valore)));

// Map con funzioni
const trasformatori = new Map<string, (s: string) => string>();
trasformatori.set("upper", (s) => s.toUpperCase());
trasformatori.set("lower", (s) => s.toLowerCase());
trasformatori.set("reverse", (s) => s.split("").reverse().join(""));

Inferenza Contestuale

TypeScript inferisce tipi dei parametri arrow function basandosi sul contesto di utilizzo, riducendo annotazioni esplicite.

// Tipi inferiti da metodi array
const numeri = [1, 2, 3];
const doppi = numeri.map((n) => n * 2); // n inferito come number

interface Prodotto {
  nome: string;
  prezzo: number;
}

const prodotti: Prodotto[] = [
  { nome: "A", prezzo: 10 },
  { nome: "B", prezzo: 20 },
];

// p inferito come Prodotto
const costosi = prodotti.filter((p) => p.prezzo > 15);

// Inferenza in callback personalizzate
function applica<T>(arr: T[], fn: (item: T) => T): T[] {
  return arr.map(fn);
}

const risultati = applica([1, 2, 3], (n) => n * 2); // n inferito come number

Performance

Arrow function hanno performance sostanzialmente identica a function tradizionali per esecuzione, ma come proprietà di classe occupano memoria per ogni istanza anziché condividere il prototipo.

class ConMetodoTradizionale {
  metodo() {} // Sul prototipo, condiviso tra istanze
}

class ConArrow {
  metodo = () => {}; // Proprietà istanza, memoria per ogni istanza
}

// Per migliaia di istanze, metodi tradizionali usano meno memoria
const istanze1 = Array.from(
  { length: 10000 },
  () => new ConMetodoTradizionale()
);
const istanze2 = Array.from({ length: 10000 }, () => new ConArrow());

Quando Usare Arrow Function

Arrow function sono preferibili per callback, operazioni funzionali, e quando serve binding automatico di this. Function tradizionali restano appropriate per metodi che devono accedere a arguments o essere usati come costruttori.

// Preferire arrow per callback
button.addEventListener("click", () => console.log("Click"));

// Preferire arrow per array operations
const risultato = arr.map((x) => x * 2);

// Preferire function per metodi prototipo quando this non è problema
Array.prototype.customMethod = function () {
  return this.length;
};

// Preferire arrow in classi per garantire this
class Component {
  onClick = () => {
    // this è sempre Component
  };
}

Conclusioni

Le arrow function in TypeScript forniscono sintassi concisa e binding lessicale di this, risultando ideali per callback, programmazione funzionale, e metodi di classe che richiedono contesto preservato. La sintassi streamlined riduce boilerplate mentre il comportamento prevedibile di this elimina una classe comune di bug. Comprendere le differenze con function tradizionali permette di scegliere lo strumento appropriato per ogni contesto, bilanciando concisione, performance, e semantica corretta.