TypeScript Mixins: oltre i limiti dell'ereditarietà singola

Nella programmazione orientata agli oggetti (OOP), siamo abituati a pensare in termini di gerarchie verticali: creiamo una classe base, la estendiamo, e poi la la specializziamo ulteriormente. Funziona, finché non ci scontriamo con il muro dell'ereditarietà singola.

In TypeScript, come in molti altri linguaggi, una classe può estendere solo un'altra classe. Ma cosa succede quando abbiamo bisogno che la nostra classe Admin erediti funzionalità sia da User (per i dati anagrafici) che da Logger (per scrivere su file) e magari anche da EventEmitter?

Le strade sono solitamente tre, ma sono tutte piene di insidie:

  • Scegliere un solo genitore e duplicare il resto del codice.
  • Creare catene di ereditarietà infinite e fragili.
  • Generare "God Classes" mastodontiche che sanno fare tutto, violando il principio di singola responsabilità.

Ma c’è una soluzione architetturale a questo problema: passare dall'ereditarietà alla Composizione, e in TypeScript lo strumento principe per realizzarla sono i Mixins.

 

Is-A vs Has-A

L'ereditarietà classica risponde alla domanda Cos'è questo oggetto? (Is-A).

  • Un Cane è un Animale.

I Mixin rispondono alla domanda Cosa sa fare questo oggetto? (Has-A / Capable-of).

  • Un Cane sa Abbaiare, sa Correre, sa EssereTracciato.

I Mixin ci permettono di prendere comportamenti isolati e "incollarli" sopra le nostre classi come adesivi, senza vincoli di gerarchia.

 

La "Formula Magica": il costruttore generico

In TypeScript, un Mixin non è altro che una funzione che accetta una classe e ne restituisce una versione estesa.

Per farlo funzionare con il sistema di tipi statico di TS, dobbiamo prima definire cos'è una "classe" agli occhi del compilatore. Tecnicamente, una classe è qualcosa che può essere istanziata tramite new.

Definiamo un tipo helper che useremo ovunque:

1// GConstructor = Generic Constructor
2// Rappresenta una qualsiasi classe che accetta un array di argomenti qualsiasi
3type GConstructor<T = {}> = new (...args: any[]) => T;

 

Creiamo il primo Mixin: Timestamped

Immaginiamo di voler aggiungere le proprietà createdAt e updatedAt a diverse entità del nostro software (Utenti, Ordini, Prodotti), senza dover riscrivere le stesse righe ogni volta.

Ecco come si scrive il Mixin:

1// 1. La funzione accetta una classe "Base"
2function Timestamped<TBase extends GConstructor>(Base: TBase) {
3  
4  // 2. Restituisce una nuova classe anonima che estende "Base"
5  return class extends Base {
6    
7    // 3. Aggiungiamo nuove proprietà
8    createdAt = new Date();
9    updatedAt = new Date();
10
11    // 4. Aggiungiamo nuovi metodi
12    touch() {
13      this.updatedAt = new Date();
14      console.log(`Oggetto aggiornato il: ${this.updatedAt.toISOString()}`);
15    }
16  };
17}

Notate la potenza di questo approccio: Timestamped non sa nulla della classe che sta estendendo. È completamente agnostico.

 

Utilizzo: costruire classi come mattoncini Lego

Ora possiamo applicare questo "superpotere" a qualsiasi classe.

1class User {
2  constructor(public name: string) {}
3}
4
5class Product {
6  constructor(public sku: string, public price: number) {}
7}
8
9// Creiamo le versioni "potenziate"
10const SmartUser = Timestamped(User);
11const SmartProduct = Timestamped(Product);
12
13// Utilizzo
14const user = new SmartUser('Mario');
15
16console.log(user.name);      // Proprietà originale di User
17console.log(user.createdAt); // Proprietà iniettata dal Mixin
18user.touch();                // Metodo iniettato dal Mixin

Senza toccare la classe User originale, le abbiamo conferito nuove capacità.

 

Approccio alternativo: applyMixins

La documentazione ufficiale di TypeScript suggerisce un secondo pattern per lavorare con i Mixin, basato su una funzione helper chiamata applyMixins. Invece di annidare funzioni, si dichiara una classe che implementa le interfacce dei comportamenti desiderati, e poi si "copia" manualmente il prototipo di ogni mixin su di essa.

1// La funzione helper (da scrivere una volta sola)
2function applyMixins(derivedCtor: any, constructors: any[]) {
3  constructors.forEach((baseCtor) => {
4    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
5      Object.defineProperty(
6        derivedCtor.prototype,
7        name,
8        Object.getOwnPropertyDescriptor(baseCtor.prototype, name)!
9      );
10    });
11  });
12}
13
14// I "blocchi" di comportamento diventano classi normali
15class Timestamped {
16  createdAt = new Date();
17  touch() { this.createdAt = new Date(); }
18}
19
20class Activatable {
21  isActive = false;
22  activate() { this.isActive = true; }
23}
24
25// La classe dichiara cosa sa fare tramite "implements"
26class SmartUser implements Timestamped, Activatable {
27  createdAt!: Date;
28  touch!: () => void;
29  isActive!: boolean;
30  activate!: () => void;
31
32  constructor(public name: string) {}
33}
34
35// La funzione helper copia i prototipi a runtime
36applyMixins(SmartUser, [Timestamped, Activatable]);

Questo approccio ha un vantaggio: la classe finale è leggibile in modo diretto, perché si vede subito tramite implements quali comportamenti incorpora. Lo svantaggio, però, è significativo: TypeScript non è in grado di verificare a compile time che applyMixins stia effettivamente copiando tutto correttamente, e le proprietà dichiarate nella classe con ! (il non-null assertion operator) sono essenzialmente una promessa manuale al compilatore, non una garanzia reale.

Per questo motivo, il pattern basato sulle funzioni visto in precedenza è generalmente preferibile: offre la stessa flessibilità compositiva mantenendo una type safety molto più solida. applyMixins resta comunque utile da conoscere, specialmente quando si lavora su codebase legacy o si integrano librerie di terze parti che seguono questo stile.

 

Livello Avanzato: constrained mixins (Mixin vincolati)

Fin qui è semplice. Ma cosa succede se vogliamo creare un Mixin che dipende da un metodo della classe base?

Immaginiamo un Mixin AutoSave che vuole salvare l'oggetto ogni volta che cambia. Per funzionare, questo Mixin deve essere sicuro che la classe base abbia un metodo save(). Se lo applichiamo a una classe che non ha save(), il codice scoppierà a runtime.

TypeScript ci permette di prevenire questo errore usando i Generic Constraints.

1// 1. Definiamo il vincolo (Il contratto)
2type CanSave = GConstructor<{ save(): void }>;
3
4// 2. Vincoliamo TBase a estendere CanSave
5function AutoSave<TBase extends CanSave>(Base: TBase) {
6  return class extends Base {
7    saveAndLog() {
8      console.log('Salvataggio automatico in corso...');
9      
10      // TypeScript qui NON dà errore, perché sa che save() esiste!
11      this.save(); 
12    }
13  };
14}

Proviamo ad usarlo:

1class Documento {
2  save() { console.log('Documento salvato su DB.'); }
3}
4
5class Nota {
6  // Nota NON ha il metodo save()
7}
8
9// ✅ Funziona: Documento rispetta il vincolo
10const SmartDoc = AutoSave(Documento);
11
12// ❌ ERRORE DI COMPILAZIONE:
13// Type 'Nota' is not assignable to type 'CanSave'.
14const SmartNota = AutoSave(Nota);

Questo pattern ci offre una Type Safety assoluta, impedendoci di applicare comportamenti a oggetti che non sono pronti a supportarli.

 

Quando usare i Mixin?

Nonostante la loro potenza, i Mixins non sono la soluzione a ogni problema e vanno usati con giudizio.

✅ I casi d'uso ideali:

  • Cross-Cutting Concerns: funzionalità trasversali come Logging, Gestione Date, Active Record (save/delete), Gestione dello stato (IsLoading/IsActive).
  • Composizione di Utility: quando hai piccole unità di logica riutilizzabili su domini diversi.
  • Librerie condivise: per distribuire funzionalità opzionali senza forzare l'utente della libreria a ereditare da una tua classe base gigante.

⛔ Quando evitarli:

  • Logica di Business Core: se Cane è un Animale, usa l'ereditarietà classica. È semanticamente più corretto.
  • Catene troppo lunghe: evita MixinA(MixinB(MixinC(Class))). Rende il debug difficile e lo stack trace confuso.

 

Conclusione

I Mixin in TypeScript ci permettono di rispettare il principio DRY (Don't Repeat Yourself) in modo elegante, spostando il focus dalla gerarchia rigida alla composizione flessibile. Se ti trovi spesso a copiare e incollare gli stessi metodi in classi diverse, è molto probabile che tu abbia appena trovato il candidato perfetto per il tuo prossimo Mixin.

Hai un progetto in mente?

Contattaci

Autore

Gabriele Bilello

Considera la programmazione più una passione che un lavoro. È la stessa curiosità che spinge molti sviluppatori a esplorare nuovi linguaggi, framework e metodologie ogni giorno.

Per lui programmare è come un videogioco: davanti a ogni bug o requisito c’è un enigma da risolvere, una sfida da affrontare nel modo più elegante ed efficiente possibile. Ama sperimentare e provare cose nuove, soprattutto quando migliorano la developer experience e rendono il lavoro più fluido e “comodo”.

Sviluppa principalmente applicazioni web con Angular, Node.js, NestJS e Astro, ma nel tempo ha lavorato anche con PHP, MySQL, e JavaScript, senza disdegnare incursioni in altri linguaggi e tecnologie come Vue o Flutter.

Nel tempo libero gli piacciono i videogiochi e i gatti — due ottime scuole di pazienza, strategia e problem solving, qualità che tornano sorprendentemente utili anche nel codice.

Devmy su linkedin

Ultimi Articoli