TypeScript è uno strumento fenomenale a cui non rinuncerei mai, specialmente in ambito enterprise. Ma se iniziare ad usarlo è estremamente facile, padroneggiarlo è l’esatto contrario. E il problema è che, se non usato appieno, può risultare anche inutile, se non addirittura dannoso per alcuni!

Quindi, iniziamo con il primo articolo di una serie chiamata Advanced Types, in cui prendiamo spunto dalla già ottima documentazione ufficiale e cerchiamo di spiegare qualche concetto avanzato senza perderci in chiacchiere o terminologie troppo difficili. Se non utilizzate già TypeScript, vi consiglio di aspettare a leggere questo articolo. Oppure leggetelo, giusto per rendervi conto delle potenzialità che offre, e tornate qui quando avrete un po’ le mani in pasta! 😉

Cos’è un Type e perché crea confusione

TypeScript ci permette di portare il nostro codice ad alti livelli di sicurezza, confidenza e manutenibilità. Detta in parole povere, se lo utilizziamo correttamente non dobbiamo più preoccuparci del fatto che una nostra funzione ci possa ritornare un valore che non ci aspettavamo.

L’utilizzo più banale di TypeScript è quello di affidarsi solamente alle primitive e ai tipi di base offerti dallo strumento, ovvero:

let numero: number;
numero = 4;

let stringa: string;
stringa = 'Ciao';

let booleano: boolean;
booleano = true;

let array: number[];
array = [1, 2, 3, 4, 5];

Mentre il secondo step che probabilmente avrete fatto, nel vostro percorso con TypeScript, sarà stato l’utilizzo delle interfacce, come questa:

interface Person {
  name: string;
  surname: string;
}

let user: Person;

E probabilmente, saprete anche che è possibile definire un Inline Type se non ci interessa riutilizzare un’interfaccia:

let user: { name: string; surname: string; };

Abbiamo poi dei tipi particolari: null, undefined, any e void. I primi due li abbiamo già in JavaScript, ma con TypeScript diventano dei veri e propri tipi. Any lo utilizziamo quando un’entità può essere di qualsiasi tipo (molto utile per i refactoring!) mentre void viene usato nelle funzioni che non ritornano valori.

Potrebbero già sorgervi dei dubbi sulla differenza fra un Type e un’interfaccia, ma questo sarà argomento di un altro articolo: per ora, sappiate che un’interfaccia è un tipo, e viene utilizzata solitamente per definire la forma di un oggetto.

Torniamo a noi: molti utenti si fermano qui, e da fanboy di TypeScript potrei dire che non c’è nulla di male, in fondo mi fa piacere promuovere il suo utilizzo, no? Invece no, perché se le vostre nozioni sull’argomento si fermano qui, rischiate di rendere rapidamente i vostri progetti un disastro, un mare di codice non manutenibile con una tipizzazione parziale, fine a sé stessa.

Vediamo un po’ di concetti per ampliare le nostre conoscenze ed evolvere il nostro codice.

Intersection Types

Un Intersection Type è il risultato dell’intersezione fra più tipi. Supponiamo di avere due interfacce di questo tipo:

interface Person {
  name: string;
}

interface Contact {
  email: string;
  phone: string;
}

Se ne abbiamo la necessità, possiamo fonderle in questo modo:

const details: Person & Contact;

details.name;  // OK
details.email; // OK
details.phone; // OK

Nulla di complicato. Ma bisogna fare attenzione ad una cosa: se i tipi che fondiamo hanno delle proprietà in comune, possono sorgere dei problemi. Ad esempio:

interface Person {
  name: string;
  phone: string;
}

interface Contact {
  email: string;
  phone: number;
}

const details: Person & Contact;

details.phone = '123456789'; // Errore!

Vedete la proprietà phone che ha due tipi diversi? In questo caso TypeScript vi darà un errore e vi dirà che una stringa non è assegnabile ad un tipo string & number. Senza senso? Per quello che abbiamo imparato finora pare di sì, ma un fondamento ce l’ha, anche se non lo vedremo ora nel dettaglio 🙂

Se invece la stessa proprietà fosse un oggetto con proprietà differenti, non ci sarebbero problemi:

interface Person {
  name: string;
  phone: {
    number: string;
    prefix: string;
  }
}

interface Contact {
  email: string;
  phone: {
    number: string;
    country: string;
  }
}

const details: Person & Contact;

details.phone.number = '123456789'; // Ok! è stringa in entrambi
details.phone.prefix = '+39'; // Ok!
details.phone.country = 'Italy'; // Ok!

Union Types

Creiamo altre due interfacce:

interface Cat {
  name: string;
  meow: () => string;
}

interface Dog {
  name: string;
  bark: () => string;
}

Molto simili fra loro, ma il modo in cui le due entità comunicano è differente: una miagola, l’altra abbaia. Immaginiamo di avere una funzione che ci ritorni un animale, cane o gatto, questo è il modo in cui dovremmo tipizzarla:

function getAnimal(): Cat | Dog {
  ...
}

In questo modo abbiamo creato uno Union Type, che possiamo anche estrapolare in un nuovo tipo nel caso dovessimo riutilizzarlo:

type Animal = Cat | Dog;

function getAnimal(): Animal { ... }

In questo modo però, il chiamante della funzione non sa se l’entità che gli viene ritornata è un cane o un gatto. Il tipo Animal, infatti, conterrà solo le proprietà comuni fra Cat e Dog, ovvero la proprietà name.

const myPet = getAnimal();

myPet.name; // Funzione
myPet.bark(); // Errore! Non sappiamo se è un cane!

Dobbiamo quindi trovare il modo di capire, dato un animale, se questo è un cane o un gatto. Vediamo come fare.

Type Guards

Abbiamo due modi per verificare il tipo di una nostra variabile: il primo è attraverso la Type Assertion e qualche linea di codice imperativo.

let myPet = getAnimal();

/**
 * Questo NON lo possiamo fare perché Typescript ci protegge!
*/
if (myPet.bark) {
    myPet.bark()
} else if (myPet.meow) {
    myPet.meow();
}

/**
 * In questo modo invece obblighiamo TypeScript a considerare
 * il nostro animale prima come un cane, e poi come un gatto.
*/
if ((myPet as Dog).bark) {
    (myPet as Dog).bark()
} else if ((myPet as Cat).meow) {
    (myPet as Cat).meow();
}

Funziona? Sì. Brutto? Sì. C’è un altro modo, più bello da scrivere e da utilizzare.

User-defined Type Guards

Fortunatamente, TypeScript è intelligente e sa che questa è una problematica comune: per questo, ci mette a disposizione delle Type Guard, degli strumenti per verificare il tipo di una variabile a run-time piuttosto che a compile-time!

Il primo fra questi strumenti è l’operatore is.

Operatore “is”

Ecco come potremmo scrivere la nostra guardia con questo operatore:

function isDog(pet: Cat | Dog): pet is Dog {
    return (pet as Dog).bark !== undefined;
}

Come vedete un po’ del codice precedente è rimasto: abbiamo utilizzato sempre la Type Assertion per obbligare TypeScript a pensare che l’animale sia un cane, abbiamo verificato se può abbaiare, ma la cosa interessante è il tipo del valore di ritorno: pet is Dog. Se avessimo ritornato un booleano, non ci sarebbero stati problemi. Ma grazie a quel tipo particolare e l’operatore is, ora possiamo scrivere questo:

if (isDog(myPet)) {
  myPet.bark();
} else {
  myPet.meow();
}

Notata la differenza? Ora non solo TypeScript sa che il nostro animale è un cane nel ramo if, ma anche nel ramo else! Va notato che l’operatore is può essere applicato solo a un parametro della funzione, non a qualsiasi variabile!

Operatore “in”

Possiamo evolvere la nostra funzione precedente: ES6 (e quindi non TypeScript, attenzione!) ci fornisce uno strumento utilissimo, l’operatore in, per verificare se un oggetto contiene una determinata proprietà. Avete già capito come modificare l’esempio precedente?

function isDog(pet: Cat | Dog): pet is Dog {
    return 'bark' in pet;
}

In questo caso, l’operatore in ci ritorna un booleano, che viene poi valutato da TypeScript per sapere se il nostro parametro pet è un cane (attraverso l’operatore is).

Typeof Guards

Un altro utile strumento è typeof, che ci permette di sapere se una nostra variabile è una stringa, un numero, un booleano o un symbol. Non importa se non sapete cos’è un symbol, non l’abbiamo ancora visto e lo vedremo con calma in un altro articolo, per ora vi basti sapere che se utilizzate typeof con qualsiasi altro tipo, non vi darà errore ma non funzionerà.

La cosa interessante di questo strumento però è la sua immediatezza: infatti non siamo obbligati a scrivere una funzione come nel caso precedente, perché TypeScript riconoscerà la nostra espressione come una guardia. Ecco un semplicissimo esempio, sapendo che non possiamo utilizzare le nostre interfacce Dog e Cat:

if (typeof numero === 'number') {
  numero++;
}

if (typeof stringa === 'string') {
  stringa + '!';
}

if (typeof booleano === 'boolean') {
  booleano = !booleano;
}

Instanceof Guards

Questo strumento invece è pensato per le istanze e di conseguenza funziona con le classi. TypeScript utilizzerà instanceof per comparare i costruttori delle classi per verificare che si tratti in effetti della stessa classe. Vediamo un esempio:

interface Animal {
  name: string;
}

class Dog implements Animal {

  constructor(public name: string) {}

  bark() {
    return 'Wooof!';
  }
}

class Cat implements Animal {

  constructor(public name: string) {}

  meow() {
    return 'Miao!';
  }
}

function getAnimal() {
    return Math.random() < 0.5 ?
        new Dog('Cane') :
        new Cat('Gatto');
}

// TypeScript lo setta a: Dog | Cat, non più Animal!
let myPet: Animal = getAnimal();

if (myPet instanceof Dog) {
    // Qui dentro, myPet è di tipo Dog
}
if (myPet instanceof Cat) {
    // Qui dentro, myPet è di tipo Cat
}

Quindi, se già vi trovate in una condizione di utilizzare delle classi, potete usare questo strumento senza problemi. Ma se usate delle interfacce, evitate di trasformarle in classi solamente per poter utilizzare instanceof: una interfaccia ha i suoi vantaggi, primo fra tutti il fatto di scomparire dal codice JavaScript generato da TypeScript, mantenendo l’applicazione leggera e performante.

Conclusioni

Abbiamo ancora molto da esplorare in TypeScript, ma abbiamo già iniziato ad addentrarci fra gli aspetti più avanzati di questa tecnologia. Per ora, ricordate che:

  • Potete utilizzare delle interfacce per dare una forma prestabilita ad un oggetto
  • Potete utilizzare delle classi se vi interessa che la vostra entità possa fornire delle funzionalità attraverso i suoi metodi
  • Potete utilizzare gli Intersection Types ( simbolo & ) per fondere due tipi: il risultato sarà a sua volta un tipo con tutte le proprietà dei due o più tipi scelti
  • Potete utilizzare gli Union Types ( simbolo | )per unire due tipi: il risultato sarà a sua volta un tipo con solo le proprietà in comune fra i tipi scelti
  • Potete utilizzare la Type Assertion per fare un check su un tipo, ma…
  • …è consigliato utilizzare altri strumenti, come is, in, typeof, instanceof, e all’evenienza creare le proprie guardie custom

Ti è piaciuto l’articolo? Condividilo!

Rilasciare risorse gratuite non è facile: un sacco di tempo vola fra la ricerca dell’idea, la stesura, la preparazione di codice funzionante e la revisione. Se ti piace ciò che facciamo, condividi l’articolo: nel peggiore dei casi ti daranno del secchione!

Ti interesserà anche…

Decoratori custom in TypeScript

In questo articolo esploriamo una delle particolarità più interessanti di TypeScript: i decoratori! Si tratta di particolari costrutti che, se usate Angular o NestJS, conoscerete già alla perfezione: ci consentono…

Iscriviti alla nostra newsletter!

Rimani aggiornato per quando rilasceremo nuovi articoli, nuovi corsinuove promozioni e nuove risorse gratuite. Ti promettiamo che useremo la tua email con prudenza, e non la condivideremo con nessuno senza il tuo consenso!

Abbiamo a cuore la tua privacy. Niente spam. Leggi la nostra privacy policy qui.

Menu