Un’interfaccia è una forma di un oggetto. Un oggetto JavaScript standard è una mappa di coppie key:value
. Le chiavi degli oggetti JavaScript in quasi tutti i casi sono stringhe e i loro valori sono qualsiasi valore JavaScript supportato (primitivo o astratto).
Un’interfaccia dice al compilatore TypeScript i nomi delle proprietà che un oggetto può avere e i loro corrispondenti tipi di valore. Pertanto, interface è un tipo ed è un tipo astratto poiché è composto da tipi primitivi.
Quando definiamo un oggetto con proprietà (chiavi) e valori, TypeScript crea un’interfaccia implicita guardando i nomi delle proprietà e il tipo di dati dei loro valori nell’oggetto. Questo accade a causa dell’inferenza dei tipi.
Nell’esempio precedente, abbiamo creato un oggetto student
con campi firstName
, lastName
, age
e getSalary
e assegnato alcuni valori iniziali. Usando queste informazioni, TypeScript crea un tipo di interfaccia implicita per student
.
{
firstName: string;
lastName: string;
age: number;
getSalary: (base: number) => number;
}
Un’interfaccia è proprio come un oggetto ma contiene solo le informazioni sulle proprietà dell’oggetto e i loro tipi. Possiamo anche creare un tipo di interfaccia e dargli un nome in modo da poterlo usare per annotare i valori degli oggetti, ma qui, questa interfaccia non ha un nome poiché è stata creata implicitamente. Potete confrontare questo con il tipo di funzione nella lezione precedente che è stato creato implicitamente all’inizio e poi abbiamo creato un tipo di funzione esplicitamente usando type alias.
Proviamo a pasticciare con le proprietà dell’oggetto dopo che è stato definito.
Come potete vedere dall’esempio precedente, TypeScript ricorda la forma di un oggetto poiché il tipo di ross
è l’interfaccia implicita. Se cerchiamo di sovrascrivere il valore di una proprietà con un valore di tipo diverso da quello specificato nell’interfaccia o cerchiamo di aggiungere una nuova proprietà che non è specificata nell’interfaccia, il compilatore TypeScript non compilerà il programma.
Se volete che un oggetto abbia fondamentalmente qualsiasi proprietà, allora potete marcare esplicitamente un valore any
e il compilatore TypeScript non dedurrà il tipo dal valore dell’oggetto assegnato. Ci sono altri modi migliori per ottenere esattamente questo e li esamineremo in questo articolo.
Dichiarazione di interfaccia
Anche se l’interfaccia implicita che abbiamo visto finora è tecnicamente un tipo, ma non è stata definita esplicitamente. Come discusso, un’interfaccia non è altro che la forma che un oggetto può assumere. Se avete una funzione che accetta un argomento che dovrebbe essere un oggetto ma di una forma particolare, allora dobbiamo annotare quell’argomento (parametro) con un tipo di interfaccia.
Nell’esempio precedente, abbiamo definito una funzione getPersonInfo
che accetta un argomento oggetto che ha firstName
, lastName
, age
e getSalary
campi di tipi di dati specificati. Notate che abbiamo usato un oggetto che contiene i nomi delle proprietà e i loro tipi corrispondenti come tipo usando l’annotazione :<type>
. Questo è un esempio di interfaccia anonima poiché l’interfaccia non ha un nome, è stata usata inline.
Questo sembra un po’ complicato da gestire. Se l’oggetto ross
diventa più complicato e ha bisogno di essere usato in più posti, TypeScript sembra una cosa che inizialmente ti piaceva, ma che ora è solo una cosa difficile da gestire. Per risolvere questo problema, definiamo un tipo di interfaccia usando la parola chiave interface
.
Nell’esempio precedente, abbiamo definito un’interfaccia Person
che descrive la forma di un oggetto, ma questa volta, abbiamo un nome che possiamo usare per riferirci a questo tipo. Abbiamo usato questo tipo per annotare la variabile ross
e l’argomento person
della funzione getPersonIfo
. Questo informerà TypeScript di validare queste entità rispetto alla forma di Person
.
Perché usare un’interfaccia?
Il tipo di interfaccia può essere importante per imporre una forma particolare. Tipicamente in JavaScript, mettiamo una fede cieca a runtime che un oggetto conterrà sempre una particolare proprietà e quella proprietà avrà sempre un valore di un particolare tipo come {age: 21, ...}
per esempio.
Quando iniziamo effettivamente ad eseguire operazioni su quella proprietà senza prima controllare se quella proprietà esiste sull’oggetto o se il suo valore è quello che ci aspettavamo, le cose possono andare male e può lasciare la vostra applicazione inutilizzabile in seguito. Per esempio, {age: '21', ...}
, qui age
il valore è un string
.
Le interfacce forniscono un meccanismo sicuro per affrontare tali scenari in fase di compilazione. Se state accidentalmente usando una proprietà su un oggetto che non esiste o usate il valore di una proprietà in un’operazione illegale, il compilatore TypeScript non compilerà il vostro programma. Vediamo un esempio.
Nell’esempio precedente, stiamo cercando di usare la proprietà name
dell’argomento _student
dentro la funzione printStudent
. Poiché l’argomento _student
è un tipo di interfaccia Student
, il compilatore TypeScript lancia un errore durante la compilazione poiché questa proprietà non esiste nell’interfaccia Student
.
Similmente, 100 — _student.firstName
non è un’operazione valida poiché la proprietà firstName
è un tipo di string
e l’ultima volta che ho controllato, non si può sottrarre un string
da un number
in JavaScript (risultati in NaN
).
Nell’esempio precedente, abbiamo usato il modo tradizionale di scrivere tipo funzione per il campo getSalary
. Tuttavia, potete anche usare la sintassi di funzione senza il corpo per la stessa, che è generalmente usata nelle interfacce.
interface Student {
firstName: string;
lastName: string;
age: number;
getSalary(base: number): number;
};
Proprietà opzionali
A volte, avete bisogno che un oggetto abbia una proprietà che contiene dati di un particolare tipo di dati, ma non è obbligatorio avere quella proprietà sull’oggetto. Questo è simile ai parametri di funzione opzionali che abbiamo imparato nella lezione precedente.
Tali proprietà sono chiamate proprietà opzionali. Un’interfaccia può contenere proprietà opzionali e noi usiamo l’annotazione ?:Type
per rappresentarle, proprio come i parametri di funzione opzionali.
Nell’esempio precedente, l’interfaccia Student
ha la proprietà age
che è opzionale. Tuttavia, se la proprietà age
viene fornita, deve avere un valore del tipo number
.
Nel caso dell’oggetto ross
che è un tipo di interfaccia Student
, non abbiamo fornito il valore per la proprietà age
che è legale, tuttavia, nel caso di monica
, abbiamo fornito la proprietà age
ma il suo valore è string
che non è legale. Quindi il compilatore TypeScript lancia un errore.
L’errore potrebbe sembrare strano ma in realtà ha senso. Se la proprietà age
non esiste su un oggetto, il object.age
restituirà undefined
che è un tipo di undefined
. Se esiste, allora il valore deve essere del tipo number
.
Quindi il valore della proprietà age
può essere del tipo undefined
o number
che in TypeScript è rappresentato usando la sintassi di unione number | undefined
.
💡 Impareremo le unioni di tipi in una lezione sul Type System.
Tuttavia le proprietà opzionali pongono seri problemi durante l’esecuzione del programma. Immaginiamo se stiamo usando la proprietà age
in un’operazione aritmetica ma il suo valore è undefined
. Questo è una specie di problema serio.
Ma la cosa buona è che il compilatore TypeScript non permette di eseguire operazioni illegali su una proprietà opzionale poiché il suo valore può essere undefined
.
Nell’esempio precedente, stiamo eseguendo un’operazione aritmetica sulla proprietà age
che è illegale perché il valore di questa proprietà può essere number
o undefined
nel runtime. Eseguendo operazioni aritmetiche su undefined
si ottiene NaN
(non un numero).
💡 Tuttavia, per il programma sopra, abbiamo dovuto impostare il flag
--strictNullChecks
sufalse
che è un flag del compilatore TypeScript. Se forniamo questa opzione, il programma di cui sopra si compila bene.
Per evitare questo errore o avvertimento, dobbiamo dire esplicitamente al compilatore TypeScript che questa proprietà è un tipo di number
e non di number
o undefined
. Per questo, usiamo la type assertion (AKA type conversion o typecasting).
Nel programma sopra, abbiamo usato (_student.age as number)
che converte il tipo di _student.age
da number | undefined
a number
. Questo è un modo per dire al compilatore TypeScript: “Ehi, questo è un numero”. Ma un modo migliore per gestire questo sarebbe controllare anche se _student.age
è undefined
in fase di runtime e poi eseguire l’operazione aritmetica.
💡 Impareremo le asserzioni di tipo in una lezione sul Type System.
Tipo di funzione usando un’interfaccia
Non solo la forma di un oggetto semplice, ma un’interfaccia può anche descrivere la firma di una funzione. Nella lezione precedente, abbiamo usato type alias per descrivere un tipo di funzione, ma anche le interfacce possono farlo.
interface InterfaceName {
(param: Type): Type;
}
La sintassi per dichiarare un’interfaccia come tipo di funzione è simile alla firma della funzione stessa. Come potete vedere dall’esempio sopra, il corpo dell’interfaccia contiene l’esatta firma di una funzione anonima, senza il corpo ovviamente. Qui i nomi dei parametri non hanno importanza.
Nell’esempio sopra, abbiamo definito IsSumOdd
interfaccia che definisce un tipo di funzione che accetta due argomenti di tipo number
e ritorna un valore boolean
. Ora potete usare questo tipo per descrivere una funzione perché il tipo di interfaccia IsSumOdd
è equivalente al tipo di funzione (x: number, y: number) => boolean
.
Un’interfaccia con una firma di metodo anonimo descrive una funzione. Ma una funzione nel regno di JavaScript è anche un oggetto, il che significa che potete aggiungere proprietà al valore di una funzione proprio come un oggetto. Quindi è perfettamente legale definire qualsiasi proprietà su un’interfaccia di tipo funzione.
Nell’esempio sopra, abbiamo aggiunto le proprietà type
e calculate
sull’interfaccia IsSumOdd
che descrive una funzione. Usando il metodo Object.assign
, stiamo unendo le proprietà type
e calculate
con un valore di funzione.
Le interfacce di tipo funzione possono essere utili per descrivere le funzioni costruttrici. Una funzione costruttore è simile a una classe il cui compito è quello di creare oggetti (istanze). Abbiamo avuto solo funzioni costruttore fino a ES5 per imitare un class
in JavaScript. Pertanto, TypeScript compila le classi in funzioni costruttore se si punta a ES5
o meno.
💡 Se vuoi saperne di più sulla funzione costruttore, segui questo articolo.
Se mettiamo la parola chiave new
prima della firma della funzione anonima nell’interfaccia, rende la funzione costruibile. Ciò significa che la funzione può essere invocata solo usando la parola chiave new
per generare oggetti e non usando una normale chiamata di funzione. Un esempio di funzione costruttore appare come sotto.
function Animal( _name ) {
this.name = _name;
}var dog = new Animal( 'Tommy' );
console.log( dog.name ); // Tommy
Per fortuna, non dobbiamo lavorare con le funzioni costruttore poiché TypeScript fornisce la parola chiave class
per creare una classe che è molto più facile da lavorare di una funzione costruttore, credetemi. Infatti, una class
in fondo è una funzione costruttrice in JavaScript. Provate il seguente esempio.
class Animal{
constructor( _name ) {
this.name = _name;
}
}console.log( typeof Animal ); // "function"
Una classe e una funzione costruttrice sono la stessa cosa. L’unica differenza è che la class
ci dà una ricca sintassi OOP con cui lavorare. Quindi, un’interfaccia di un tipo di funzione costruttrice rappresenta una classe.
Nell’esempio precedente, abbiamo definito la classe Animal
con una funzione costruttore che accetta un argomento di tipo string
. Puoi considerare questo come una funzione costruttore che ha una firma simile a quella del costruttore Animal
.
Il AnimalInterface
definisce una funzione costruttore poiché ha funzione anonima prefissata con la parola chiave new
. Questo significa che la classe Animal
si qualifica per essere un tipo di AnimalInterface
. Qui, il tipo di interfaccia AnimalInterface
è equivalente al tipo di funzione new (sound: string) => any
.
La funzione createAnimal
accetta ctor
argomento di tipo AnimalInterface
, quindi possiamo passare la classe Animal
come valore dell’argomento. Non saremo in grado di aggiungere la firma del metodo getSound
della classe Animal
in AnimalInterface
e il motivo è spiegato nella lezione Classi.
Tipi indicizzabili
Un oggetto indicizzabile è un oggetto le cui proprietà possono essere raggiunte usando una firma indice come obj
. Questo è il modo predefinito per accedere ad un elemento di un array, ma possiamo farlo anche per l’oggetto.
var a = ;
var o = { one: 1, two: 2, three: 3 };console.log( a ); // 1
console.log( a ); // 1 (same as `a.one`)
A volte, il vostro oggetto può avere un numero arbitrario di proprietà senza una forma definita. In questo caso, potete semplicemente usare il tipo object
. Tuttavia, questo tipo object
definisce qualsiasi valore che non sia number
, string
, boolean
, symbol
, null
, o undefined
come discusso nella lezione sui tipi di base.
Se abbiamo bisogno di controllare strettamente se un valore è un semplice oggetto JavaScript allora potremmo avere un problema. Questo può essere risolto usando un tipo di interfaccia con una firma di indice per il nome della proprietà.
interface SimpleObject {
: any;
}
L’interfaccia SimpleObject
definisce la forma di un oggetto con string
chiavi i cui valori possono essere any
di tipo dati. Qui, il nome della proprietà key
è usato solo come segnaposto poiché è racchiuso tra parentesi quadre.
Nell’esempio precedente, abbiamo definito ross
e monica
oggetto di tipo SimpleObject
interfaccia. Poiché questi oggetti contengono chiavi string
e valori di tipo any
, è perfettamente legale.
Se siete confusi sulla chiave 1
nel monica
che è un tipo di number
, questo è legale poiché gli oggetti o gli array in JavaScript possono essere indicizzati usando chiavi number
o string
, come mostrato sotto.
var o = { 0: 'Zero', '1': 'One' };
var a = ;console.log( o ); // Zero
console.log( o ); // One
console.log( a ); // One
console.log( a ); // One
Se abbiamo bisogno di essere più precisi sul tipo di chiavi e i loro valori, possiamo sicuramente fare anche questo. Per esempio, possiamo definire un tipo di interfaccia indicizzabile con chiavi di tipo number
e valori di tipo number
se vogliamo.
💡 Un tipo di chiave della firma dell’indice deve essere o
string
onumber
.
Nell’esempio precedente, abbiamo definito un’interfaccia LapTimes
che può contenere nomi di proprietà di tipo number
e valori di tipo number
. Questa interfaccia può rappresentare una struttura di dati che può essere indicizzata usando chiavi number
, quindi l’array ross
e gli oggetti monica
e joey
sono legali.
Tuttavia, l’oggetto rachel
non è conforme alla forma di LapTimes
poiché la chiave one
è un string
e si può accedere solo usando string
come rachel
e niente altro. Quindi il compilatore TypeScript darà un errore come mostrato sopra.
È possibile avere alcune proprietà obbligatorie e altre opzionali in un tipo di interfaccia indicizzabile. Questo può essere abbastanza utile quando abbiamo bisogno che un oggetto abbia una certa forma ma non importa se abbiamo proprietà extra e indesiderate nell’oggetto.
Nell’esempio precedente, abbiamo definito un’interfaccia LapTimes
che deve contenere la proprietà name
con valore string
e la proprietà opzionale age
con valore number
. Un oggetto di tipo LapTimes
può anche avere proprietà arbitrarie le cui chiavi devono essere number
e i cui valori devono essere anch’essi number
.
L’oggetto ross
è un oggetto LapTimes
valido anche se non ha la proprietà age
poiché è opzionale. Tuttavia, monica
ha la proprietà age
ma il suo valore è string
quindi non è conforme all’interfaccia LapTimes
.
Anche l’oggetto joey
non è conforme all’interfaccia LapTimes
poiché ha una proprietà gender
che è un tipo di string
. L’oggetto rachel
non ha la proprietà name
che è richiesta nell’interfaccia LapTimes
.
💡 Ci sono alcuni inconvenienti a cui dobbiamo prestare attenzione quando usiamo tipi indicizzabili. Questi sono menzionati in questa documentazione.
Extending Interface
Come le classi, un’interfaccia può ereditare proprietà da altre interfacce. Tuttavia, a differenza delle classi in JavaScript, un’interfaccia può ereditare da più interfacce. Usiamo la parola chiave extends
per ereditare un’interfaccia.
Estrando un’interfaccia, l’interfaccia figlia ottiene tutte le proprietà dell’interfaccia madre. Questo è abbastanza utile quando più interfacce hanno una struttura comune e vogliamo evitare la duplicazione del codice portando le proprietà comuni in un’interfaccia comune che può essere successivamente ereditata.
Nell’esempio precedente, abbiamo creato un’interfaccia Student
che eredita proprietà dall’interfaccia Person
e Player
.
Dichiarazioni di interfacce multiple
Nella sezione precedente, abbiamo imparato come un’interfaccia può ereditare le proprietà di un’altra interfaccia. Questo è stato fatto usando la parola chiave extend
. Tuttavia, quando interfacce con lo stesso nome sono dichiarate all’interno dello stesso modulo (file), TypeScript fonde le loro proprietà insieme a patto che abbiano nomi di proprietà distinti o che i loro tipi di proprietà in conflitto siano gli stessi.
Nell’esempio precedente, abbiamo dichiarato Person
interfaccia diverse volte. Questo risulterà in una singola dichiarazione di interfaccia Person
fondendo le proprietà di tutte le dichiarazioni di interfaccia Person
.
💡 Nella lezione sulle classi, abbiamo imparato che una
class
dichiara implicitamente un’interfaccia e un’interfaccia può estendere tale interfaccia. Quindi se un programma ha una classePerson
e un’interfacciaPerson
, allora il tipo finalePerson
(interfaccia) avrà proprietà fuse tra la classe e l’interfaccia.
Interfacce annidate
Un’interfaccia può avere strutture profondamente annidate. Nell’esempio seguente, il campo info
dell’interfaccia Student
definisce la forma di un oggetto con firstName
e lastName
proprietà.
Parimenti, è perfettamente legale per un campo di un’interfaccia avere il tipo di un’altra interfaccia. Nell’esempio seguente, il campo info
dell’interfaccia Student
ha il tipo dell’interfaccia Person
.