Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Estrarre la logica dei tipi in TypeScript

Estrarre la logica dei tipi rende TypeScript riusabile. Ecco perché devono diventare una struttura capace di seguire la logica del dominio
Estrarre la logica dei tipi rende TypeScript riusabile. Ecco perché devono diventare una struttura capace di seguire la logica del dominio
Link copiato negli appunti

C'è un momento preciso, in quasi ogni progetto TypeScript, in cui ci si accorge che i tipi non stanno più svolgendo un ruolo puramente descrittivo. All'inizio servono soprattutto a evitare errori banali, a chiarire la forma di un oggetto, a documentare meglio parametri e valori di ritorno. Poi il progetto cresce, si moltiplicano i modelli, arrivano le API, i componenti riutilizzabili, le trasformazioni tra dati interni ed esterni. A quel punto i tipi non bastano più come etichette. Devono diventare una struttura capace di seguire la logica del dominio.

È proprio qui che diventa importante estrarre la logica dei tipi. L'espressione può sembrare un po' astratta, ma in realtà descrive qualcosa di molto concreto. Significa evitare di riscrivere più volte versioni simili dello stesso tipo e iniziare invece a costruire relazioni tra tipi, lasciando che siano il compilatore e le regole di TypeScript a derivare ciò che serve.

In un progetto piccolo la duplicazione passa quasi inosservata. Si definisce un tipo per l'utente, poi un altro per il form, poi un altro ancora per l'aggiornamento del profilo. All'apparenza tutto fila liscio. Il problema emerge quando una proprietà cambia nome, oppure diventa facoltativa, oppure non deve più comparire in una certa risposta. Se i tipi sono stati scritti a mano, uno per uno, il rischio di incoerenza cresce immediatamente.

Estrarre la logica dei tipi serve a evitare proprio questo. Non si tratta di fare virtuosismi, né di trasformare TypeScript in un esercizio di stile. Si tratta di far sì che le regole siano esplicite e riutilizzabili, invece di essere sparse in definizioni ripetute.

Il problema dei tipi duplicati in TypeScript

Uno degli errori più comuni in TypeScript è creare molti tipi quasi identici per contesti leggermente diversi. È una tentazione comprensibile, perché sul momento sembra il modo più rapido per procedere. Si parte magari da un'entità principale come User, poi si aggiungono UserForm, UserUpdate, UserResponse, UserPreview. Ogni definizione appare lecita, ma dopo qualche mese il quadro comincia a complicarsi.

Il punto critico è che questi tipi non sono davvero indipendenti. Descrivono quasi sempre la stessa entità vista da angolazioni diverse. Se però vengono definiti manualmente, il legame tra loro resta implicito. Dipende dalla memoria di chi legge o modifica il codice, non da una relazione esplicita verificata dal compilatore.

Immaginiamo un caso semplice:

type User = {
  id: string
  name: string
  email: string
  password: string
  createdAt: string
}

type PublicUser = {
  id: string
  name: string
  email: string
  createdAt: string
}

Questo codice funziona, ma il problema è evidente. PublicUser è quasi uguale a User, solo che manca password. Se domani aggiungiamo una nuova proprietà a User, dobbiamo ricordarci di aggiornare anche PublicUser. Se non lo facciamo, i due tipi cominceranno a divergere in silenzio.

Estrarre la logica dei tipi significa dire a TypeScript: questo secondo tipo non è autonomo, deriva dal primo secondo una regola precisa. Per esempio:

type User = {
  id: string
  name: string
  email: string
  password: string
  createdAt: string
}

type PublicUser = Omit

Qui la relazione è chiara. PublicUser non è più una copia manuale, ma il risultato di una trasformazione. Se User cambia, anche PublicUser si aggiorna in modo coerente.

Dalla descrizione alla trasformazione

Questo è il vero salto di qualità. Finché i tipi si limitano a descrivere, restano statici. Quando invece cominciano a trasformare altri tipi, il sistema diventa molto più potente.

TypeScript mette a disposizione diversi strumenti per fare questo. Alcuni sono diventati quasi familiari, come Pick, Omit, Partial o Readonly. Non sono semplici scorciatoie: rappresentano un modo diverso di pensare ai tipi. Invece di scrivere ogni variante a mano, si definisce una regola di derivazione.

Prendiamo il caso di un payload di aggiornamento. Di solito, quando si aggiorna una risorsa, non è necessario inviare tutti i campi, ma solo quelli modificati. Una definizione manuale potrebbe essere questa:

type UpdateUser = {
  name?: string
  email?: string
  password?: string
}

Anche qui però c'è un legame evidente con il tipo di partenza. La versione più solida consiste nel derivarlo:

type User = {
  id: string
  name: string
  email: string
  password: string
  createdAt: string
}

type UpdateUser = Partial<Omit>

In questo modo stiamo dicendo qualcosa di molto più interessante. Non stiamo solo descrivendo un oggetto. Stiamo formalizzando una regola del dominio: un aggiornamento utente può contenere una parte dei dati modificabili, ma non i campi generati dal sistema.

Estrarre la logica significa rendere esplicite le regole

Il vantaggio più importante di questo approccio è che le regole smettono di vivere solo nella documentazione o nella testa degli sviluppatori. Entrano direttamente nel sistema dei tipi.

Se, per esempio, un'applicazione distingue tra dati completi e dati esposti pubblicamente, quella distinzione non dovrebbe essere affidata a definizioni scritte a mano. Dovrebbe diventare una relazione esplicita. Se un form di creazione richiede alcuni campi, ma un form di modifica li rende facoltativi, anche questo è un comportamento che può essere modellato a livello di tipi.

Consideriamo un altro esempio molto frequente, quello dei dati ricevuti dal backend e dei dati usati nell'interfaccia. Può capitare che dal server arrivi una data in formato stringa, mentre nel frontend si preferisca lavorare con un oggetto Date o con un altro formato più comodo. Senza estrazione della logica, si finisce per riscrivere molte varianti manuali. Con un approccio più maturo, invece, si può centralizzare meglio la trasformazione concettuale.

Naturalmente non tutto può o deve essere risolto solo coi tipi. TypeScript non sostituisce la logica runtime. Però può rendere estremamente chiaro il rapporto tra una struttura e le sue varianti.

Il ruolo di keyof e dei mapped types in TypeScript

Quando si comincia a estrarre davvero la logica dei tipi, uno degli strumenti più importanti è keyof. Serve a ottenere l'unione delle chiavi di un tipo e permette di ragionare sulla sua struttura in modo dinamico.

Immaginiamo di avere questo tipo:

type Product = {
  id: string
  title: string
  price: number
  inStock: boolean
}

Con keyof Product otteniamo una unione di stringhe che rappresenta le sue proprietà, cioè "id" | "title" | "price" | "inStock".

Questo diventa molto utile quando si vogliono costruire tipi che dipendono direttamente dalle proprietà di un altro tipo. Per esempio potremmo voler creare un oggetto di validazione in cui ogni campo del prodotto ha associato un messaggio o una funzione di controllo. Invece di scriverlo a mano, possiamo derivarlo.

type Product = {
  id: string
  title: string
  price: number
  inStock: boolean
}

type ProductErrors = {
  [K in keyof Product]?: string
}

Qui entra in gioco il mapped type. Stiamo dicendo a TypeScript di attraversare tutte le chiavi di Product e costruire un nuovo tipo con le stesse chiavi, ma con valori di tipo string facoltativi. In questo modo la struttura degli errori segue automaticamente quella del modello.

Se domani aggiungiamo una proprietà come category, il tipo ProductErrors la includerà senza bisogno di alcun aggiornamento manuale. Questo è un esempio molto concreto di logica estratta: la forma di un tipo viene generata da una regola, non riscritta a mano.

Tipi generici: quando la regola vale per più modelli

L'estrazione della logica diventa ancora più interessante quando una stessa trasformazione si ripete su entità diverse. In questi casi conviene fare un ulteriore passo avanti e usare i generics.

Supponiamo di voler descrivere un tipo che rappresenti la versione di sola lettura di qualunque modello. Potremmo scrivere una variante per ogni entità, ma sarebbe uno spreco. Possiamo invece definire una regola generica.

type ReadonlyFields = {
  readonly [K in keyof T]: T[K]
}

A questo punto possiamo usarla con qualsiasi struttura:

type Order = {
  id: string
  total: number
}
type ReadonlyOrder = ReadonlyFields

L'utilità non sta tanto nell'aver reinventato Readonly, che esiste già nella libreria standard, quanto nell'aver capito il principio. Un tipo può essere trattato come input di una trasformazione. Quando questo meccanismo entra nel modo di pensare di chi scrive TypeScript, il codice cambia qualità.

Un altro esempio molto realistico riguarda la rimozione di campi sensibili. Potremmo creare una utility riutilizzabile per tutte le entità che contengono informazioni da non esporre.

type WithoutSensitiveFields = Omit

Questa utility è semplice e forse perfino troppo rigida per alcuni contesti, ma rende bene l'idea. Non stiamo più creando tipi isolati. Stiamo definendo una regola che può essere riapplicata con coerenza.

Ti consigliamo anche