Un'interfaccia dà un nome alla forma di un oggetto: le proprietà che deve avere e il tipo di ciascuna. Una volta dichiarata, usi il nome come tipo, e il compilatore controlla rispetto a essa ogni oggetto che passi, restituisci o assegni.
L'ultima chiamata è l'errore di compilazione TS2741. Le interfacce vengono cancellate quando il codice è compilato: nell'output JavaScript non resta traccia di User, e nessuno controlla la forma a runtime.
Dichiarare un'interfaccia
La sintassi è la parola chiave interface, un nome (in PascalCase per convenzione) e un corpo che elenca i membri. I membri possono essere separati da punti e virgola, virgole o semplici a capo; il punto e virgola è lo stile più comune.
interface Product {
sku: string; // required property
price: number;
tags: string[]; // array property
dimensions: { // nested object type
width: number;
height: number;
};
discount?: number; // optional property
readonly createdAt: Date; // cannot be reassigned
label(): string; // method
}
Un'interfaccia è un tipo, non un valore. Non si può istanziare con new, non ha valori predefiniti, e obj instanceof Product è l'errore TS2693 ('Product' only refers to a type, but is being used as a value here). Per controllare una forma a runtime, scrivi un type guard.
Tipizzazione strutturale e controllo delle proprietà in eccesso
TypeScript confronta forme, non nomi. Qualsiasi oggetto con le proprietà richieste è compatibile con l'interfaccia, che sia stato dichiarato con essa o no. Le proprietà in più vanno bene, con un'eccezione: un oggetto letterale scritto direttamente dove è attesa l'interfaccia subisce un controllo delle proprietà in eccesso, perché lì una chiave sconosciuta è quasi sempre un errore di battitura.
Quell'errore (TS2353) è ciò che intercetta { id: 1, name: "a", emial: "x" } per un User: il compilatore suggerisce perfino Did you mean to write 'email'? (TS2561).
Proprietà opzionali e readonly
Un ? dopo il nome rende una proprietà opzionale: l'oggetto può ometterla, e leggerla dà T | undefined. readonly vieta di riassegnare la proprietà dopo che l'oggetto è stato creato.
Qui si vedono due limiti. Primo, readonly vale solo in fase di compilazione: le due righe marcate @ts-expect-error vengono comunque eseguite quando premi Run, e riescono, e le ultime righe cambiano apiUrl attraverso un riferimento tipizzato senza readonly. Secondo, è superficiale: readonly hosts: string[] impedirebbe di riassegnare hosts ma permetterebbe ancora hosts.push(...), ed è per questo che l'array stesso è tipizzato readonly string[]. Documenta e fa rispettare un'intenzione nel codice tipizzato; non congela niente. L'utility type Readonly<T> rende readonly in un colpo solo ogni proprietà di un'interfaccia esistente.
Metodi e proprietà funzione
Un metodo si può scrivere come firma di metodo, name(params): ReturnType, o come proprietà che contiene una funzione, name: (params) => ReturnType. Chi li chiama li usa allo stesso modo.
La differenza è sottile: con strictFunctionTypes (parte di strict), i parametri delle proprietà di tipo funzione sono controllati in modo rigoroso, mentre le firme di metodo sono controllate in modo più permissivo (bivariante), quindi la forma a proprietà intercetta qualche errore in più. La sintassi a metodo è più corta e più diffusa; vanno bene entrambe.
Un'interfaccia può anche descrivere qualcosa di invocabile o costruibile, con una call signature o una construct signature:
interface Formatter {
(value: number): string; // call signature: the object is a function
locale: string; // and it also has a property
}
interface PointConstructor {
new (x: number, y: number): { x: number; y: number }; // construct signature
}
Index signature
Quando i nomi delle proprietà non sono noti in anticipo, una index signature li descrive tutti insieme: [key: string]: T significa "qualsiasi chiave stringa, ognuna con un T".
Le ultime righe mostrano il tranello: leggere una chiave che non esiste è tipizzato come number, non number | undefined. L'opzione del compilatore noUncheckedIndexedAccess aggiunge | undefined a ogni lettura di questo tipo.
Le proprietà con nome possono stare accanto a una index signature, ma devono essere compatibili con essa. interface Dict { [key: string]: number; name: string } è l'errore TS2411, Property 'name' of type 'string' is not assignable to 'string' index type 'number'. Allarga il tipo dell'indice ([key: string]: number | string) o sposta la parte dinamica in una proprietà a sé. Per semplici mappe chiave/valore, Record<string, number> dice la stessa cosa in una riga.
Estendere un'interfaccia
extends costruisce una nuova interfaccia a partire da una o più interfacce esistenti. La figlia ha ogni membro del genitore più i propri:
interface Animal {
name: string;
}
interface Pet extends Animal {
owner: string;
}
interface Trained {
commands: string[];
}
interface ServiceDog extends Pet, Trained {
certifiedUntil: Date;
}
// ServiceDog requires: name, owner, commands, certifiedUntil
Una figlia può ridichiarare una proprietà del genitore solo con un tipo compatibile (più stretto), come kind: "dog" dove il genitore dice kind: string. Le regole, e come estendere gli alias di tipo, sono nella pagina su extends.
Implementare un'interfaccia in una classe
class X implements Shape chiede al compilatore di controllare che la classe abbia tutto ciò che l'interfaccia richiede. Un membro mancante è un errore nella dichiarazione della classe:
index.ts(7,7): error TS2420: Class 'Circle' incorrectly implements interface 'Shape'.
Property 'area' is missing in type 'Circle' but required in type 'Shape'.
Con area() aggiunto, diverse classi e perfino un oggetto semplice possono essere usati come Shape:
implements è solo un controllo. Non aggiunge membri alla classe, e non tipizza al posto tuo i parametri dei metodi della classe: greet(name) {} dentro una classe che implementa greet(name: string): string è comunque l'errore TS7006, Parameter 'name' implicitly has an 'any' type. Una classe può implementare più interfacce: class A implements B, C.
Declaration merging
Dichiarare due volte un'interfaccia con lo stesso nome nello stesso scope unisce le due in una sola. È qualcosa che gli alias di tipo non possono fare (un secondo type con lo stesso nome è un errore di identificatore duplicato).
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
// Settings now requires both properties
const s: Settings = { theme: "dark", fontSize: 14 };
Nel codice applicativo raramente è ciò che vuoi, e un merge accidentale può confondere. Il suo vero uso è aggiungere membri a tipi che non ti appartengono: le opzioni di una libreria, o un globale come Window. Da dentro un modulo, racchiudi la dichiarazione in declare global:
declare global {
interface Window {
analytics: { track(event: string): void };
}
}
export {};
Dopo questo, window.analytics.track("signup") supera il controllo dei tipi in tutto il progetto. I pacchetti di definizioni di tipo si basano sullo stesso meccanismo; vedi file di dichiarazione.
Valori predefiniti per le proprietà di un'interfaccia
Un'interfaccia non può contenere valori predefiniti, perché descrive tipi e viene cancellata a runtime. size?: "sm" | "md" = "md" è l'errore TS1246, An interface property cannot have an initializer. Rendi la proprietà opzionale e inserisci il default dove l'oggetto viene usato:
I default della destrutturazione sono la scelta più sicura: si applicano ogni volta che il valore è undefined, compreso un size: undefined esplicito. La versione con lo spread copia quell'undefined esplicito sopra il default, e il suo risultato è comunque tipizzato come se size fosse sempre impostato. Se vuoi quella garanzia dai tipi, attiva exactOptionalPropertyTypes, che rende size: undefined un errore di compilazione per un size?: ... opzionale. Una classe con campi inizializzati è l'altra opzione quando l'oggetto ha bisogno anche di un comportamento.
Interfacce generiche
Un'interfaccia può ricevere parametri di tipo, così una sola dichiarazione funziona per molti tipi di payload:
interface ApiResponse<T> {
ok: boolean;
data: T;
error?: string;
}
interface Page<T> {
items: T[];
nextCursor?: string;
}
interface User {
id: number;
name: string;
}
const res: ApiResponse<Page<User>> = {
ok: true,
data: { items: [{ id: 1, name: "Ada" }], nextCursor: "abc" },
};
ApiResponse<Page<User>> si legge come "una risposta i cui dati sono una pagina di utenti". La libreria standard ne è piena: Array<T>, Promise<T>, Map<K, V> sono tutte interfacce generiche.
Interface o type alias
Un alias type può descrivere la stessa forma di oggetto, e per i semplici tipi oggetto i due sono intercambiabili. Solo un'interfaccia può fare merging; solo un alias di tipo può dare un nome a un'unione, una tupla, o un mapped o conditional type. La regola pratica dell'handbook di TypeScript è usare interface finché non ti serve una funzionalità che ha solo type. La pagina interface vs type ha il confronto completo, compresa la differenza con Record<string, ...> che sorprende la maggior parte delle persone.
Domande frequenti
Cos'è un'interfaccia in TypeScript?
Un'interfaccia è una descrizione con nome della forma di un oggetto: i nomi delle proprietà, i loro tipi, quali sono opzionali o readonly, e i suoi metodi. Il compilatore controlla che i valori usati come quell'interfaccia abbiano quella forma. Le interfacce esistono solo in fase di compilazione; non producono JavaScript.
Come si imposta un valore predefinito in un'interfaccia TypeScript?
Non si può: un'interfaccia descrive tipi, non valori, quindi size: "md" = ... non è una sintassi valida. Rendi la proprietà opzionale (size?: "sm" | "md") e applica il valore predefinito dove l'oggetto viene usato, di solito con i default della destrutturazione nei parametri della funzione: function render({ size = "md" }: Options). Funziona anche lo spread di un oggetto di default ({ ...DEFAULTS, ...options }), ma un undefined esplicito in options sovrascrive il default.
Come verifico a runtime se un oggetto implementa un'interfaccia?
Non esiste un modo integrato, perché le interfacce vengono cancellate durante la compilazione: obj instanceof User è l'errore TS2693 ('User' only refers to a type, but is being used as a value here). Scrivi una funzione type guard che controlli le proprietà, function isUser(x: unknown): x is User { ... }, oppure valida con una libreria di schemi.
Un'interfaccia può estendere più interfacce?
Sì. Elencale dopo extends, separate da virgole: interface ServiceDog extends Pet, Trained { ... }. La nuova interfaccia ha tutti i membri di ciascun genitore più i propri. Se due genitori dichiarano la stessa proprietà con tipi incompatibili, la dichiarazione è un errore.
Qual è la differenza tra un'interfaccia e una classe in TypeScript?
Una classe esiste a runtime: ha un costruttore, implementazioni dei metodi, e new crea oggetti a partire da essa. Un'interfaccia descrive solo una forma per il compilatore e viene cancellata dall'output JavaScript. Una classe può dichiarare implements SomeInterface perché il compilatore controlli che corrisponda, e anche qualsiasi oggetto semplice con la forma giusta è compatibile con l'interfaccia.