back to top

Le interfacce in TypeScript

Nella precedente lezione abbiamo introdotto il sistema dei tipi presente in TypeScript e abbiamo visto come usare la tecnica opzionale delle annotazioni con i tipi predefiniti. È comunque possibile definire dei tipi personalizzati. Un primo modo è quello mostrato nel frammento di codice che segue.

const car: {modello: string, marca: string} = {modello: 'E-Type', marca: 'Jaguar'};

La costante car contiene un riferimento a un oggetto che deve avere due proprietà: modello (di tipo string) e marca (sempre di tipo string). Si tratta di un nuovo tipo da noi definito a cui possiamo dare un nome attraverso l’uso delle interfacce.

Esempio di Interfaccia in TypeScript

Riformuliamo l’esempio appena visto e definiamo un’interfaccia Car.

interface Car {
  modello: string,
  marca: string
}

const car: Car = {modello: 'E-Type', marca: 'Jaguar'};

Un’interfaccia definisce un contratto (o specifica). Chiunque implementi quell’interfaccia, dovrà rispettare la struttura da essa definita e avere le proprietà e i metodi specificati nel contratto.

TypeScript usa il sistema detto Duck Typing o Structural Subtyping per controllare se un oggetto rispetta la struttura definita dall’interfaccia.

Col termine Duck Typing ci si riferisce a un particolare stile di tipizzazione di certi linguaggi in cui il valore semantico di un un oggetto è determinato dalla sua struttura, ovvero dalle sue proprietà e metodi. (Il termine Duck Typing è riconducibile al Duck Test solitamente illustrato dalla frase: "Se cammina come un’anatra e starnazza come un’anatra, è molto probabile che si tratti di un’anatra").

Per verificare quindi se un oggetto è compatibile con un’interfaccia, non si fa riferimento al suo tipo ma alla sua struttura e si verifica che abbia certi metodi e proprietà. Affinché un oggetto rispetti il contratto definito dall’interfaccia, deve contenere le proprietà e i metodi dell’interfaccia.

Proprietà delle interfacce opzionali

Nell’esempio riportato sotto abbiamo aggiunto una proprietà all’interfaccia Car. Si tratta della proprietà anno. Se notate bene, abbiamo usato un punto interrogativo che serve a indicare che la proprietà è opzionale. L’oggetto alfa dell’esempio non presenta infatti la proprietà anno, ma si tratta sempre di un oggetto di tipo Car visto che, come detto, la proprietà anno non è obbligatoria.

interface Car {
  modello: string,
  marca: string,
  anno?: number
}

const jaguar: Car = {
  modello: 'E-Type', 
  marca: 'Jaguar',
  anno: 1961
};

// anno è una proprietà 
const alfa: Car = {
  modello: 'Montreal', 
  marca: 'Alfa Romeo',
};

Proprietà delle interfacce di sola lettura

Se poi, in fase di definizione di un’interfaccia, anteponiamo la keyword readonly al nome di una proprietà, indichiamo che quest’ultima può essere settata solo nel momento in cui l’oggetto viene creato. Non è possibile modificare il suo valore successivamente.

interface Car {
  modello: string,
  marca: string,
  readonly anno?: number
}

const jaguar: Car = {
  modello: 'E-Type', 
  marca: 'Jaguar',
  anno: 1961
};

// anno è una proprietà opzionale
const alfa: Car = {
  modello: 'Montreal', 
  marca: 'Alfa Romeo',
};

/* 
  Cannot assign to 'anno' because it is a constant or a read-only property.
*/
jaguar.anno = 1962; // ERRORE

Consideriamo un altro esempio in cui definiamo un’interfaccia di tipo Motorcycle come segue.

interface Motorcycle {
  modello: string;
  marca: string;
  anno?: string | number;
  [prop: string]: any
}

let moto1: Motorcycle = {
  modello: 'V7 Special',
  marca: 'Moto Guzzi',
  anno: 1971
}

let moto2: Motorcycle = {
  modello: '916',
  marca: 'Ducati',
  colore: 'rosso',
  serbatoio: '17l'
}

Un oggetto di tipo Motorcycle deve avere almeno due proprietà modello e marca che devono essere di tipo string. La proprietà anno non è obbligatoria e può essere di tipo string o number. Può inoltre avere qualsiasi altra proprietà che non sia una di quelle appena elencate e che può essere di qualsiasi tipo.

Le interfacce per i tipi indicizzabili

Nel caso degli array useremo la sintassi riportata sotto per indicare che l’indice di ciascun elemento è di tipo numerico. Il valore può essere di qualsiasi tipo. Nell’esempio mostrato definiamo un array in cui ogni elemento è un oggetto di tipo Car e ha un indice numerico.

interface Car {
  modello: string,
  marca: string,
  readonly anno?: number
}

const jaguar: Car = {
  modello: 'E-Type', 
  marca: 'Jaguar',
  anno: 1961
};

// anno è una proprietà 
const alfa: Car = {
  modello: 'Montreal', 
  marca: 'Alfa Romeo',
};

// interfaccia per CarArray
// indici dell'array di tipo 'number'
interface CarArray {
  [index: number]: Car;
}

let arr: CarArray = [
  jaguar,
  alfa
];

/*
  Type '{ modello: string; }' is not assignable to type 'Car'.
  Property 'marca' is missing in type '{ modello: string; }'.
*/
let arr2: CarArray = [
  jaguar,
  {modello: 'Duna'} // ERRORE
]

Notate che all’interno dell’interfaccia CarArray abbiamo usato una sitassi particolare ([index: number]: Car) per indicare che gli elementi dell’array hanno degli indici di tipo number e devono essere degli oggetti di tipo Car. Se infatti proviamo ad aggiungere a un array di tipo CarArray l’oggetto avente come unica proprietà {modello: ‘Duna’}, TypeScript mostra un errore il quale segnala che non si tratta di un oggetto di tipo Car.

È possibile usare una sintassi simile per creare degli oggetti in cui l’indice deve essere però di tipo string. È un modo per creare degli oggetti che possono avere un numero indefinito di proprietà che devono tuttavia essere del tipo definito nell’interfaccia.

interface Dizionario {
  [index: string]: string;
}

let d1: Dizionario = {
  italiano: 'ciao',
  inglese: 'hello'
}

let d2: Dizionario = {
  italiano: 'ciao',
  spagnolo: 'hola',
  olandese: 'hallo'
}

Ogni oggetto di tipo Dizionario può avere un numero indefinito di proprietà purché siano di tipo string.

Le interfacce e le funzioni in TypeScript

È possibile usare le interfacce per definire la segnatura di una funzione come mostrato nell’esempio che segue.

interface Operazione {
  (a: number, b: number): number;
}

let somma: Operazione;
let moltiplicazione: Operazione;

somma = function (addendo1: number, addendo2:number): number {
  return addendo1 + addendo2;
};

moltiplicazione = (moltiplicando: number, moltiplicatore: number): number => {
  return moltiplicando * moltiplicatore;
};

Abbiamo definito due funzioni che implementano l’interfaccia Operazione, si tratta infatti di funzioni che ricevono in ingresso due numeri e restituiscono sempre un valore numerico.

Ereditarità e interfacce in TypeScript

Ogni interfaccia può estendere una o più interfacce ereditando le proprietà e i metodi in esse definiti. Vediamo allora un altro esempio in cui riprendiamo il codice già visto sopra.

interface Car {
  modello: string,
  marca: string,
  readonly anno?: number
}

const jaguar: Car = {
  modello: 'E-Type', 
  marca: 'Jaguar',
  anno: 1961
};

// anno è una proprietà opzionale
const alfa: Car = {
  modello: 'Montreal', 
  marca: 'Alfa Romeo',
};

interface CarArray  {
  [index: number]: Car;
}

let arr: CarArray = [
  jaguar,
  alfa
];

// Property 'push' does not exist on type 'CarArray'
arr.push({
  modello: 'W113',
  marca: 'Mercedes-Benz'
})

Come possiamo vedere, se proviamo ad aggiungere un elemento all’array con il metodo push, ci viene mostrato un messaggio di errore il quale ci informa che tale metodo non è definito per il tipo CarArray. Per risolvere il problema estendiamo l’interfaccia Array<Car>. In particolare Array<T> è un tipo parametrizzato di cui parleremo nelle prossime lezioni quando introdurremo i cosiddetti Generics o tipi generici.

interface Car {
  modello: string,
  marca: string,
  readonly anno?: number
}

const jaguar: Car = {
  modello: 'E-Type', 
  marca: 'Jaguar',
  anno: 1961
};

// anno è una proprietà opzionale
const alfa: Car = {
  modello: 'Montreal', 
  marca: 'Alfa Romeo',
};

// CarArray estende ora Array<Car>
interface CarArray extends Array<Car> {
  [index: number]: Car;
}

let arr: CarArray = [
  jaguar,
  alfa
];

// possiamo ora eseguire la funzione push() senza problemi
arr.push({
  modello: 'W113',
  marca: 'Mercedes-Benz'
})

Conclusioni

In questa lezione abbiamo discusso delle interfacce che rappresentano uno dei concetti fondamentali di TypeScript. Abbiamo volontariamente omesso il caso di classi che implementano delle interfacce visto che affronteremo l’argomento in una delle prossime lezioni in cui parleremo appunto delle classi in TypeScript. Nella prossima lezione di questa guida discuteremo in dettaglio delle funzioni in TypeScript.

Pubblicitร