Menu

Interface in TypeScript: sintassi, proprietà opzionali, metodi ed esempi

Un'interfaccia dà un nome alla forma di un oggetto: quali proprietà ha e di che tipo sono. Scopri come dichiararne una, proprietà opzionali e readonly, metodi, index signature, estensione, implementazione in una classe, declaration merging, interfacce generiche e come dare valori predefiniti a un'interfaccia.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

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.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA