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àtypeinterface
Union, tuple, mapped typessìno
Declaration mergingno (errore)sì
Estensioneintersezione &parola chiave extends
Indice implicitosì, per i type literalno
Messaggi di errorespesso più leggibili sulle unionpiù 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 con interface se devono essere estesi.
  • Vieta React.FC: tipa le props e restituisci ReactNode.
  • Attiva strict e noUncheckedIndexedAccess: senza, i tipi sono decorazione.
  • Nessun as nei 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`.