React con TypeScript significa scrivere i componenti in file .tsx in cui props, stato, ref e gestori di eventi hanno dei tipi, così il compilatore intercetta una prop mancante o un gestore di eventi sbagliato prima che il codice venga eseguito. I tipi spariscono al momento della build: ciò che gira nel browser è lo stesso JavaScript che avresti scritto senza di essi. Gli editor dal vivo di questa pagina eseguono quel JavaScript, e la versione tipizzata sta accanto a ciascuno in un blocco statico.
Ecco lo stesso Badge con i tipi. count è facoltativo (?), quindi il secondo badge ricade sul valore predefinito 0, e children accetta sia un elemento sia una semplice stringa.
type BadgeProps = {
label: string;
count?: number;
children: React.ReactNode;
};
function Badge({ label, count = 0, children }: BadgeProps) {
return (
<div>
<strong>{label}</strong> ({count})
<div>{children}</div>
</div>
);
}
<Badge count={3}>Hi</Badge>;
// Error: Property 'label' is missing in type '{ children: string; count: number; }'
Creare un progetto TypeScript
Vite ha un template React più TypeScript:
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
Ottieni file .tsx, un tsconfig.json con strict attivo e le definizioni di tipo di React (@types/react, @types/react-dom) già installate. Vite rimuove i tipi mentre serve e costruisce l'app ma non li controlla; lo script build del template esegue prima tsc, e il tuo editor li controlla mentre scrivi. Framework come Next.js configurano TypeScript allo stesso modo quando crei un progetto con la loro CLI.
Tipizzare le props
Descrivi le props come un tipo oggetto e annota il parametro destrutturato. Pochi schemi coprono la maggior parte dei componenti:
interface UserCardProps {
name: string; // required
age?: number; // optional, may be undefined
role: 'admin' | 'member'; // a union of allowed strings
tags: string[];
onSelect: (id: string) => void; // a callback prop
children?: React.ReactNode; // anything React can render
}
function UserCard({ name, age, role, tags, onSelect, children }: UserCardProps) {
// ...
}
type o interface. Entrambi funzionano per le props. interface può essere estesa con extends e unita dichiarandola due volte; type può esprimere anche union, intersezioni e mapped type. La maggior parte dei team ne sceglie uno e lo usa ovunque.
children. Usa React.ReactNode per qualsiasi cosa vada tra i tag: elementi, stringhe, numeri, array, null. La pagina sulle props spiega come arriva children.
Perché React.FC è facoltativo. const Badge: React.FC<BadgeProps> = (...) => ... tipizza l'intera funzione invece del parametro. Dai tipi di React 18 non aggiunge più children per te, non può ricevere un parametro di tipo per un componente generico e non aggiunge nulla che un parametro tipizzato non dia. Il codice più vecchio lo usa molto; le semplici funzioni con props tipizzate sono la scelta comune per il codice nuovo.
Tipizzare useState
Per un valore iniziale semplice basta l'inferenza: useState(0) è un number, useState('') una string. Passa un argomento di tipo quando lo stato può contenere più di quanto suggerisca il suo valore iniziale: un valore che parte come null, o una tra diverse stringhe fisse.
Clicca "Load user" e lo stato passa da idle a loading a done. Lo stato tipizzato:
type User = { id: number; name: string };
type Status = 'idle' | 'loading' | 'done';
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<Status>('idle');
user.name; // Error: 'user' is possibly 'null'
user?.name; // OK
setStatus('ready'); // Error: Argument of type '"ready"' is not assignable to parameter of type 'SetStateAction<Status>'
Senza <User | null>, useState(null) deduce il tipo null e setUser(data) non compila. La union ti costringe a gestire il caso vuoto, che è esattamente il controllo user ? ... : ... nell'esempio in esecuzione.
useRef per gli elementi DOM
Un ref che punta a un elemento DOM riceve il tipo dell'elemento e parte da null, perché l'elemento non esiste finché React non lo collega dopo il primo rendering.
Scrivi qualcosa, poi clicca "Log the value": la console stampa ciò che hai scritto, letto direttamente dall'elemento DOM. La versione tipizzata:
const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current is HTMLInputElement | null
inputRef.current?.focus(); // OK
inputRef.current.focus(); // Error: 'inputRef.current' is possibly 'null'
const timerId = useRef<number | null>(null); // a mutable value, not a DOM node
Usa l'interfaccia dell'elemento che corrisponde al tag: HTMLInputElement, HTMLDivElement, HTMLCanvasElement, HTMLButtonElement. Nei tipi di React 19 useRef riceve sempre un argomento e restituisce un RefObject il cui current puoi assegnare, quindi la stessa forma funziona per i ref al DOM e per valori come l'id di un timer. Vedi useRef per il lato di esecuzione.
Tipi dei gestori di eventi
I gestori in linea non hanno bisogno di annotazioni: in onChange={(e) => setName(e.target.value)} TypeScript conosce e dalla prop a cui viene passato. Annota quando sposti un gestore in una funzione separata.
Invia e la console registra l'indirizzo; preventDefault impedisce alla pagina di ricaricarsi. Con i tipi:
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setEmail(e.target.value); // e.target is HTMLInputElement, value is string
}
function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
e.preventDefault();
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') { /* ... */ }
}
Da @types/react 19.2.10, onSubmit è tipizzato con React.SubmitEvent e il vecchio React.FormEvent è segnato come deprecato; con una versione più vecchia dei tipi, scrivi invece React.FormEvent<HTMLFormElement>. Il parametro di tipo è l'elemento a cui è collegato il gestore. Se non sei sicuro del tipo, passa il mouse sulla prop onChange nel tuo editor, oppure tipizza l'intero gestore in una volta: const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { ... }.
Tipizzare il context
Dai a createContext il tipo del valore. Se esiste un valore predefinito sensato, passalo, e ogni consumer riceve un valore non null:
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');
const theme = useContext(ThemeContext); // Theme
Quando non c'è un buon valore predefinito (un utente che ha effettuato l'accesso, uno store), parti da null e racchiudi useContext in un hook che genera un errore se un componente viene usato fuori dal provider. Ogni chiamante riceve allora il tipo non null e un messaggio di errore chiaro invece di un crash in profondità.
type Auth = { user: string; logout: () => void };
const AuthContext = createContext<Auth | null>(null);
export function useAuth(): Auth {
const value = useContext(AuthContext);
if (!value) throw new Error('useAuth must be used inside <AuthProvider>');
return value;
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState('Ada');
return (
<AuthContext value={{ user, logout: () => setUser('') }}>
{children}
</AuthContext>
);
}
<AuthContext value={...}> è la sintassi del provider di React 19; <AuthContext.Provider value={...}> è tipizzato allo stesso modo.
Componenti generici
Una lista, una tabella o una select che funziona con qualsiasi tipo di elemento è un componente generico: un parametro di tipo collega la prop items alla callback renderItem.
Il List tipizzato:
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
getKey: (item: T) => string | number;
};
function List<T>({ items, renderItem, getKey }: ListProps<T>) {
return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}
<List items={books} getKey={(b) => b.isbn} renderItem={(b) => b.title} />;
// b is inferred as the book type, so b.titel would be an error
TypeScript deduce T da items, quindi chi lo usa non scrive mai il parametro di tipo. In un file .tsx una arrow function ha bisogno di <T,> invece di <T>, perché <T> da solo viene letto come un tag JSX.
Estendere le props degli elementi nativi
Un wrapper attorno a un button o a un input dovrebbe accettare ogni attributo accettato dall'elemento nativo. React.ComponentProps<'button'> è quell'insieme completo; aggiungi sopra le tue props e fai lo spread del resto.
type ButtonProps = React.ComponentProps<'button'> & {
variant?: 'primary' | 'ghost';
};
function Button({ variant = 'primary', style, ...rest }: ButtonProps) {
return (
<button
{...rest}
style={{ fontWeight: variant === 'primary' ? 600 : 400, ...style }}
/>
);
}
<Button type="submit" disabled onClick={(e) => console.log(e.currentTarget)}>
Save
</Button>;
onClick, disabled, type e aria-* sono tutti tipizzati senza doverli elencare. In React 19 ref è una prop normale, e ComponentProps<'button'> la include, quindi <Button ref={buttonRef}> raggiunge il nodo DOM tramite lo spread senza forwardRef. Usa ComponentPropsWithoutRef<'button'> quando vuoi escludere ref, e ComponentProps<typeof UserCard> per leggere le props di uno dei tuoi componenti.
Errori di tipo comuni
"'ref.current' is possibly 'null'". L'elemento non esiste durante il primo rendering. Usa ref.current?.focus(), oppure controlla if (ref.current) prima di usarlo.
"Type 'string' is not assignable" su uno stato union. const [status, setStatus] = useState('idle') deduce string, che è più ampio di quanto vuoi, e passarlo a una prop tipizzata Status fallisce. Scrivi useState<Status>('idle').
Evento tipizzato con l'elemento sbagliato. React.ChangeEvent<HTMLInputElement> su una select dà a e.target il tipo sbagliato. Il parametro di tipo deve corrispondere all'elemento a cui è collegato il gestore.
Usare any per zittire un errore. Disattiva il controllo per tutto ciò che quel valore tocca. Preferisci unknown e restringilo, oppure correggi il tipo alla sua origine.
Domande frequenti
Come creo un'app React con TypeScript?
Esegui npm create vite@latest my-app -- --template react-ts, poi npm install e npm run dev. I componenti vanno in file .tsx e Vite toglie i tipi durante la build; esegui tsc (lo fa lo script build del template) per controllarli.
Qual è il tipo di children in React?
React.ReactNode. Accetta tutto ciò che React può renderizzare: elementi, stringhe, numeri, array, null e undefined. Usa React.ReactElement solo quando ti serve esattamente un elemento.
Come tipizzo useState con null?
Passa il tipo in modo esplicito come union: useState<User | null>(null). Senza l'argomento di tipo TypeScript deduce null come unico valore ammesso.
Qual è il tipo di un evento onChange in React?
React.ChangeEvent<HTMLInputElement> per un input (HTMLTextAreaElement o HTMLSelectElement per quegli elementi). e.target.value è quindi tipizzato come string.
Dovrei usare React.FC?
È facoltativo. Tipizzare direttamente il parametro delle props (function Card({ title }: CardProps)) fa lo stesso lavoro, si legge più facilmente e funziona con i generics. React.FC non aggiunge più children implicitamente, quindi ti dà poco in più.
Per le props devo usare type o interface?
Funzionano entrambi. interface può essere estesa e unita; type può esprimere union e mapped type. Scegli una convenzione per la codebase e mantienila.