Errori e fix
TypeScript: interface o type per le props di un componente
Le due forme non sono intercambiabili, ma la differenza conta in pochi casi. Ecco la regola che tiene il codice leggibile: type per le props, interface per un'API che altri devono estendere.
In sintesi
- `type` accetta union e tuple, `interface` no: per le props di un componente vince `type`.
- `interface` si può estendere con una seconda dichiarazione, utile solo per API pubbliche o augmentation.
- La differenza di prestazioni del compilatore è trascurabile rispetto alla leggibilità.
Risposta breve: per le props di un componente usa type. Per un'API che altre squadre devono estendere, usa interface. Il resto è preferenza, e la preferenza non deve cambiare da file a file.
Il meccanismo: registro dei tipi contro espressione di tipo
Una interface introduce un nome nel registro dei tipi e lo lascia aperto: chiunque può aggiungere una seconda dichiarazione con lo stesso nome e il compilatore le fonde. Un type lega un nome a un'espressione di tipo: la espressione può essere qualsiasi cosa, la dichiarazione è chiusa e una seconda con lo stesso nome è un errore.
Sul caso semplice il compilatore risolve entrambe nella stessa struttura e l'assegnabilità è identica. La differenza compare quando la forma deve esprimere una scelta o quando deve restare aperta: sono i due assi che contano davvero.
Le differenze che contano
| Proprietà | type | interface |
|---|---|---|
| Union, tuple, mapped types | sì | no |
| Declaration merging | no (errore) | sì |
| Estensione | intersezione & | parola chiave extends |
| Indice implicito | sì, per i type literal | no |
| Messaggi di errore | spesso più leggibili sulle union | più vicini al nome |
L'indice implicito merita una riga: un type literal è assegnabile a un indice solo se non dichiara proprietà incompatibili, mentre un'interfaccia non riceve mai un indice implicito. È il motivo per cui i due costrutti si comportano in modo diverso quando tipizzi la risposta di un'API.
La forma consigliata per le props
type Tone = "ok" | "warn" | "danger";
type AlertProps = {
tone: Tone;
title: string;
children?: React.ReactNode;
};
export function Alert({ tone, title, children }: AlertProps) {
return (
{title}
{children}
);
}
Il tipo derivato da una union impedisce di passare una stringa arbitraria: l'errore arriva in editor, non a runtime. Con una interface dichiareresti tone: string e perderesti il controllo, perché una union non entra in una clausola extends.
Dove vince interface
// packages/ui/src/theme.ts
export interface Theme {
colors: Record;
}
// un'app può aggiungere campi senza modificare il pacchetto
declare module "@acme/ui" {
interface Theme {
radii?: Record;
}
}
Il declaration merging è utile per i punti di estensione pubblici. È anche il motivo per cui non vuoi usarlo per le props interne: due dichiarazioni con lo stesso nome si sommano e un campo rimosso altrove continua a esistere.
Diagnosi: cerca il cast, non la parola chiave
Quando TypeScript accetta un valore sbagliato, la forma della dichiarazione non c'entra. Il tipo è stato allargato da qualche parte: un as, un any implicito, un parametro senza annotazione. Cerca i cast prima di riscrivere il tipo.
grep -rn " as [A-Za-z]\+Props\b" src | head
npx tsc --noEmit --strict
Il secondo comando dice se strict è attivo. Con strictNullChecks spento, una prop opzionale e una obbligatoria sono indistinguibili, e nessuna delle due forme può proteggerti. La prova è un esperimento: togli il cast, ricompila, guarda dove arriva l'errore. Se l'errore si sposta sul chiamante, il difetto era il cast.
Quando la correzione comune peggiora le cose
La prima scorciatoia è cambiare parola chiave per far sparire un messaggio. Su una union non compila, quindi si torna indietro e si spezza la union in tre tipi: ora il codice passa e la scelta fra i tre stati non è più verificata dal compilatore. Il messaggio era l'unico posto in cui la regola era scritta.
La seconda è aggiungere un'index signature o un'intersezione con Record<string, unknown> per chiudere un errore di proprietà sconosciuta. Da quel momento ogni chiave è valida: gli errori di battitura tornano a runtime e l'autocompletamento smette di essere un elenco.
Un limite da mettere in conto: il merging è globale
Il declaration merging si applica al programma, non al file. Due pacchetti che augmentano lo stesso Theme si sommano ovunque, anche nei test e nelle build di altri team. Se un modulo rende obbligatorio un campo che un altro non conosce, la rottura arriva lontano dal punto in cui è stata introdotta. Con un type lo stesso conflitto è un errore immediato, ed è il compromesso che paghi quando scegli l'estensione aperta.
Come prevenirlo
- Regola nel progetto: props con
type, oggetti di dominio condivisi coninterfacese devono essere estesi. - Vieta
React.FC: tipa le props e restituisciReactNode. - Attiva
strictenoUncheckedIndexedAccess: senza, i tipi sono decorazione. - Nessun
asnei file dei componenti: se serve, si annota il punto di ingresso dei dati.
Dove continuare
Nel catalogo: componenti, dashboard con shell e blocchi. Le guide: TypeScript o JavaScript per la UI e UI kit, libreria o design system; il confronto: librerie React misurate.
Domande frequenti
Quale delle due è più veloce da compilare?
La differenza si misura solo su progetti enormi, quindi non è un criterio di scelta. Prima viene la capacità di esprimere il tipo, poi la leggibilità per chi arriva dopo.
Perché `interface` accetta una seconda dichiarazione con lo stesso nome?
È il declaration merging, pensato per aggiungere campi a tipi esistenti. Con `type` la seconda dichiarazione è un errore: è una protezione, non un limite.
Posso usare `interface` con una union?
No, una union richiede un alias di tipo: `type Tone = "ok" | "warn" | "danger"`. La clausola `extends` di un'interfaccia accetta solo un nome o un elenco di nomi.
Cosa cambia per i tipi degli eventi?
Nulla: arrivano da React e non li dichiari tu. La scelta fra i due costrutti riguarda soltanto le forme che scrivi nel progetto.
Serve `React.FC`?
No, e molte codebase lo stanno togliendo. Tipare le props e restituire `ReactNode` è più diretto e non aggiunge comportamenti impliciti come `children`.