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.

(object-shape.ts)

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.

(shape-override.ts)

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.

(shape-override-any.ts)

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.

(argomento-con-forma.ts)

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.

(argument-with-interface.ts)

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.

(interface-safety.ts)

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.

(optional-properties.ts)

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.

(optional-properties-safety.ts)

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 su false 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).

(optional-properties-safety-override.ts)

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.

(function-signature.ts)

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.

(function-signature-with-methods.ts)

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.

(function-signature-with-new.ts)

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.

(indexable-type-object.ts)

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 o number.

(indexable-type-number.ts)

Nell’esempio precedente, abbiamo definito un’interfaccia LapTimesche 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.

(indexable-type-required-properties.ts)

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.

(extend-interface.ts)

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.

(extend-multiple-interfaces.ts)

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.

(merged-interface.ts)

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 classe Person e un’interfaccia Person, allora il tipo finale Person (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à.

(nested-shape.ts)

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.

(nested-interface.ts)

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.