Articolo originale: Learn TypeScript – The Ultimate Beginners Guide

TypeScript è diventato sempre più popolare negli ultimi anni e molte offerte di lavoro ora richiedono agli sviluppatori di conoscere TypeScript.

Ma non allarmarti, se conosci già JavaScript, sarai in grado di acquisire rapidamente TypeScript.

Anche se non hai intenzione di utilizzare TypeScript, impararlo ti darà una migliore comprensione di JavaScript e ti renderà uno sviluppatore migliore.

In questo articolo imparerai:

  • Cos'è TypeScript e perché dovresti impararlo
  • Come impostare un progetto con TypeScript
  • Tutti i principali concetti di TypeScript (tipi, interfacce, generici, conversione di tipo e molto altro ..)
  • Come usare TypeScript con React

Ho anche creato un compendio di TypeScript in PDF e un poster che riassume questo articolo in una pagina. Ciò semplifica la ricerca e la revisione rapida di concetti/sintassi.

TypeScript cheat sheet PDF
Cheat sheet TypeScript in PDF

Cos'è TypeScript?

TypeScript è un superset di JavaScript, il che significa che fa tutto ciò che fa JavaScript, ma con alcune funzionalità aggiuntive.

Il motivo principale per utilizzare TypeScript è aggiungere la tipizzazione statica a JavaScript. Tipizzazione statica significa che il tipo di una variabile non può essere modificato in nessun punto del programma. Può prevenire MOLTI bug!

D'altra parte, JavaScript è un linguaggio tipizzato dinamicamente, il che significa che le variabili possono cambiare tipo. Ecco un esempio:

// JavaScript
let foo = "hello";
foo = 55; // foo è stato modificato da stringa a numero - nessun problema

// TypeScript
let foo = "hello";
foo = 55; // ERRORE - foo non può cambiare da stringa a numero

TypeScript non può essere compreso dai browser, quindi deve essere compilato in JavaScript dal Compilatore TypeScript (TSC - TypeScript Compiler ), di cui parleremo presto.

Vale la pena usare TypeScript?

Perché dovresti usare TypeScript

  • Una ricerca ha dimostrato che TypeScript può rilevare il 15% dei bug comuni.
  • Leggibilità – è più facile vedere cosa dovrebbe fare il codice. E quando si lavora in squadra, è più facile vedere cosa intendevano fare gli altri sviluppatori.
  • È popolare – la conoscenza di TypeScript ti consentirà di candidarti per ulteriori buone offerte di lavoro.
  • Imparare TypeScript ti darà una migliore comprensione e una nuova prospettiva su JavaScript.

Svantaggi di TypeScript

  • Occore più tempo per scrivere del codice TypeScript rispetto a JavaScript, poiché devi specificare i tipi, quindi per progetti individuali più piccoli potrebbe non valere la pena usarlo.
  • TypeScript deve essere compilato, il che può richiedere tempo, specialmente nei progetti più grandi.

Tuttavia il tempo extra che devi dedicare alla scrittura di codice più preciso e alla compilazione, sarà più che risparmiato dal minore numero di bug che avrai nel tuo codice.

Per molti progetti, in particolare quelli di dimensioni medio-grandi, TypeScript ti farà risparmiare un sacco di tempo e mal di testa.

Se conosci già JavaScript, TypeScript non sarà troppo difficile da imparare. È un ottimo strumento da avere nel tuo arsenale.

Come Impostare un Progetto TypeScript

Installare Node e il Compilatore TypeScript

Per prima cosa, assicurati di avere installato Node nella tua macchina.

Poi installa il compilatore  TypeScript a livello globale sulla tua macchina eseguendo il seguente comando:

npm i -g typescript

Per verificare se l'installazione è andata a buon fine (in caso di successo restituirà il numero di versione) esegui:

tsc -v

Come Compilare TypeScript

Apri il tuo editor di testo e crea un file TypeScript (ad esempio index.ts).

Scrivi un po' di codice JavaScript o TypeScript:

let sport = 'football';

let id = 5;

Ora possiamo compilarlo in JavaScript con il seguente comando:

tsc index

TSC compilerà il codice in Javascript e lo scriverà in un file chiamato index.js:

var sport = 'football';
var id = 5;

Se vuoi specificare il nome del file in uscita:

tsc index.ts --outfile file-name.js

Se vuoi che il TSC compili automaticamente il tuo codice, ogniqualvolta ci sia una modifica, aggiungi l'opzione  "watch" (osserva) :

tsc index.ts -w

Una cosa interessante di TypeScript è che segnala errori nel tuo editor di testo mentre stai codificando, ma compilerà sempre il tuo codice, indipendentemente dal fatto che ci siano errori o meno.

Per esempio, quanto segue farà sì che TypeScript segnali immediatamente un errore:

var sport = 'football';
var id = 5;

id = '5'; // Errore: Il tipo 'string' non si può assegnare al tipo 'number'.

Tuttavia, se proviamo a compilare questo codice con tsc index, il codice verrà comunque compilato, nonostante l'errore.

Questa è una proprietà importante di TypeScript: presuppone che lo sviluppatore ne sappia di più. Anche se c'è un errore, TypeScript non interferisce con la compilazione del codice. Ti dice che c'è un errore, ma sta a te decidere se fare qualcosa al riguardo.

Come Impostare il File di Configurazione di TypeScript

Il file di configurazione di TypeScript dovrebbe trovarsi nella directory radice del tuo progetto. In questo file possiamo specificare i file radice, le opzioni del compilatore e quanto vogliamo che TypeScript sia rigoroso nel controllare il nostro progetto.

Per prima cosa, crea il file di configurazione di TypeScript:

tsc --init

Ora dovresti vedere un file tsconfig.json nella cartella radice del tuo progetto.

Ecco alcune opzioni di cui è bene essere a conoscenza (usando un framework frontend con TypeScript, la maggior parte di queste cose sono gestite per te):

{
    "compilerOptions": {
        ...
        /* Moduli */
        "target": "es2016", // Cambia in "ES2015" per compilare per ES6
        "rootDir": "./src", // Da dove compilare
        "outDir": "./public", // Verso dove compilare (in genere la cartella che verrà distribuita al server web)
        
        /* Supporto JavaScript*/
        "allowJs": true, // Consente la compilazione dei file Javascript
        "checkJs": true, // Verifica di tipo per i file Javascript e rileva gli errori
        
        /* Generazione */
        "sourceMap": true, // Crea un mappa dei file sorgente per i file Javascript generati (utile per il debugging)
         "removeComments": true, // Non generare commenti
    },
    "include": ["src"] // Fa sì che solo i file nella cartella src siano compilati
}

Per compilare tutto e osservare i cambiamenti:

tsc -w

Nota: quando i file in input sono specificati nella riga di comando (per esempio tsc index), i file all'interno di tsconfig.json sono ignorati.

Tipi in TypeScript

Tipi Primitivi

In JavaScript, un valore primitivo è un dato che non è un oggetto e non ha metodi. Ci sono 7 tipi di dato primitivo:

  • string
  • number
  • bigint
  • boolean
  • undefined
  • null
  • symbol

I primitivi sono immutabili: non possono essere alterati. È importante non confondere un tipo primitivo in quanto tale con la variabile assegnata a un tipo primitivo. Si potrebbe attribuire alla variabile un nuovo valore, ma quello esistente non può essere cambiato nel modo nel quale lo sono oggetti, array e funzioni.

Ecco un esempio:

let name = 'Danny';
name.toLowerCase();
console.log(name); // Danny - il metodo di string non cambia il valore

let arr = [1, 3, 5, 7];
arr.pop();
console.log(arr); // [1, 3, 5] - il metodo di array ha cambiato l'array

name = 'Anna' // L'assegnazione dà al primitivo un valore nuovo (non uno modificato)

In JavaScript, tutti i valori primitivi (a parte null e undefined) hanno oggetti  equivalenti che racchiudono i valori primitivi. Questi oggetti sono String, Number, BigInt, Boolean e Symbol. Essi forniscono i metodi che consentono ai valori primitivi di essere manipolati.

Tornando a TypeScript, possiamo impostare il tipo che vogliamo associare a una variabile aggiungendo  : tipo (chiamato "annotazione di tipo" o "firma di tipo" dopo la dichiarazione di una variabile. Esempi:

let id: number = 5;
let firstname: string = 'danny';
let hasDog: boolean = true;

let unit: number; // Dichiara la variabile senza assegnare un valore
unit = 5;

Di solito è meglio non dichiarare esplicitamente il tipo, poiché TypeScript deduce automaticamente il tipo di una variabile (deduzione del tipo):

let id = 5; // TS sa che è un numero
let firstname = 'danny'; // TS sa che è una stringa
let hasDog = true; // TS sa che è un booleano

hasDog = 'yes'; // ERRORE

Possiamo anche impostare una variabile in modo che possa essere un tipo union. Un tipo union è una variabile alla quale può essere assegnato più di un tipo:

let age: string | number;
age = 26;
age = '26';

Tipi Riferimento

In JavaScript, quasi "tutto" è un oggetto. In effetti (e in modo confuso), stringhe, numeri e booleani possono essere oggetti se definiti con la parola chiave new:

let firstname = new String('Danny');
console.log(firstname); // String {'Danny'}

Ma quando parliamo di tipi riferimento in JavaScript, ci riferiamo ad array, oggetti e funzioni.

Avvertenza: tipi primitivi vs tipi riferimento

Per coloro che non hanno mai studiato i tipi primitivi rispetto a quelli riferimento, discutiamo la differenza fondamentale.

Se a una variabile viene assegnato un tipo primitivo, possiamo pensare che quella variabile contenga il valore primitivo. Ogni valore primitivo è conservato in una posizione univoca in memoria.

Se abbiamo due variabili, x e y, ed entrambe contengono dati primitivi, allora sono completamente indipendenti l'una dall'altra:

Primitive data are stored in unique memory locations
Sia X che Y contengono dati primitivi univoci e indipendenti
let x = 2;
let y = 1;

x = y;
y = 100;
console.log(x); // 1 (anche se y è stato modificato in 100, x è ancora 1)

Questo non è il caso per i tipi riferimento. I tipi riferimento puntano alla posizione di memoria dove l'oggetto è conservato.

Reference types memory locations
point1 e point2 contengono un riferimento all'indirizzo dove è conservato l'oggetto
let point1 = { x: 1, y: 1 };
let point2 = point1;

point1.y = 100;
console.log(point2.y); // 100 (point1 e point2 fanno riferimento allo stesso indirizzo di memoria dove l'oggetto point è conservato)

Questa è stata una rapida panoramica dei tipi primitivi rispetto a quelli  riferimento. Dai un'occhiata a questo articolo se hai bisogno di una spiegazione più approfondita: Primitive vs reference types (risorsa in lingua originale inglese).

Array in TypeScript

In TypeScript, puoi definire quale tipo di dato può contenere un array:

let ids: number[] = [1, 2, 3, 4, 5]; // può contenere solo numeri
let names: string[] = ['Danny', 'Anna', 'Bazza']; // può contenere solo  stringhe
let options: boolean[] = [true, false, false]; // può contenere solo true o false
let books: object[] = [
  { name: 'Fooled by randomness', author: 'Nassim Taleb' },
  { name: 'Sapiens', author: 'Yuval Noah Harari' },
]; // può contenere solo oggetti
let arr: any[] = ['hello', 1, true]; // any in pratica fa sì che TypeScript si comporti come Javascript

ids.push(6);
ids.push('7'); // ERRORE: L'argumento di tipo 'string' non è assegnabile al parametro di tipo 'number'.

Puoi usare tipi unione per definire array che contengono tipi di dato diversi:

let person: (string | number | boolean)[] = ['Danny', 1, true];
person[0] = 100;
person[1] = {name: 'Danny'} // Errore - array person non può contenere oggetti

Se inizializzi una variabile con un valore, non è necessario dichiararne esplicitamente il tipo, poiché TypeScript lo dedurrà:

let person = ['Danny', 1, true]; // Uguale all'esempio precedente
person[0] = 100;
person[1] = { name: 'Danny' }; // Errore - array person non può contenere oggetti

Esiste un tipo speciale di array che può essere definito in TypeScript: Tuple. Una tupla è un array con dimensioni fisse e tipi di dato noti. Sono più specifici dei normali array.

let person: [string, number, boolean] = ['Danny', 1, true];
person[0] = 100; // Errore - Valore a indice 0 può essere solo string

Oggetti in TypeScript

Gli oggetti in TypeScript devono avere tutte le corrette proprietà e tipi di valore:

// Dichiara una variabile chiamata person con una annotazione di tipo oggetto specifica
let person: {
  name: string;
  location: string;
  isProgrammer: boolean;
};

// Assegna person a un oggetto con tutte le proprietà e tipi di valore richiesti
person = {
  name: 'Danny',
  location: 'UK',
  isProgrammer: true,
};

person.isProgrammer = 'Yes'; // ERRORE: dovrebbe essere boolean


person = {
  name: 'John',
  location: 'US',
}; 
// ERRORE: manca la proprietà isProgrammer

Quando si definisce la firma di un oggetto, di solito si utilizza un'interfaccia. Questo è utile se dobbiamo verificare che più oggetti abbiano le stesse proprietà e tipi di valore specifici:

interface Person {
  name: string;
  location: string;
  isProgrammer: boolean;
}

let person1: Person = {
  name: 'Danny',
  location: 'UK',
  isProgrammer: true,
};

let person2: Person = {
  name: 'Sarah',
  location: 'Germany',
  isProgrammer: false,
};

Possiamo anche dichiarare le proprietà di una funzione con le firme di funzione. Possiamo farlo usando le comuni funzioni JavaScript vecchia scuola (sayHi), o le funzioni freccia di ES6 (sayBye):

interface Speech {
  sayHi(name: string): string;
  sayBye: (name: string) => string;
}

let sayStuff: Speech = {
  sayHi: function (name: string) {
    return `Hi ${name}`;
  },
  sayBye: (name: string) => `Bye ${name}`,
};

console.log(sayStuff.sayHi('Heisenberg')); // Hi Heisenberg
console.log(sayStuff.sayBye('Heisenberg')); // Bye Heisenberg

Nota che nell'oggetto sayStuff, sayHi o sayBye potrebbero essere funzioni freccia o comuni funzioni JavaScript  – a TypeScript non interessa.

Funzioni in TypeScript

Possiamo definire di quale tipo dovrebbero essere gli argomenti della funzione, così come il tipo restituito della funzione:

// Definisce una funzione chiamata circle che ottiene una variabile di tipo number e ritorna un valore di tipo string
function circle(diam: number): string {
  return 'The circumference is ' + Math.PI * diam;
}

console.log(circle(10)); // The circumference is 31.41592653589793

La stessa funzione, con una funzione freccia ES6:

const circle = (diam: number): string => {
  return 'The circumference is ' + Math.PI * diam;
};

console.log(circle(10)); // The circumference is 31.41592653589793

Nota come non sia necessario affermare esplicitamente che circle è una funzione; TypeScript lo deduce. TypeScript deduce anche il tipo restituito della funzione, quindi anche questo non è necessario che sia dichiarato. Tuttavia, se la funzione è di grandi dimensioni, ad alcuni sviluppatori piace indicare esplicitamente il tipo restituito per chiarezza.

// Uso della tipizzazione esplicita 
const circle: Function = (diam: number): string => {
  return 'The circumference is ' + Math.PI * diam;
};

// Tipizzazione dedotta - TypeScript vede che circle è una funzione che ritorna sempre una stringa, quindi non occorre esplicitamente dichiararlo
const circle = (diam: number) => {
  return 'The circumference is ' + Math.PI * diam;
};

Possiamo aggiungere un punto interrogativo dopo un parametro per renderlo opzionale. Nota come qui sotto c è un tipo union che può essere number o string:

const add = (a: number, b: number, c?: number | string) => {
  console.log(c);

  return a + b;
};

console.log(add(5, 4, 'I could pass a number, string, or nothing here!'));
// I could pass a number, string, or nothing here!
// 9

Si dice che una funzione che non restituisce niente, restituisce void – una completa mancanza di qualsiasi valore. Di seguito, è stato esplicitamente indicato il tipo di restituzione come void. Ma ancora una volta, questo non è necessario poiché TypeScript lo dedurrà.

const logMessage = (msg: string): void => {
  console.log('This is the message: ' + msg);
};

logMessage('TypeScript is superb'); // This is the message: TypeScript is superb

Se vogliamo dichiarare una variabile di funzione, ma non definirla (dire esattamente cosa fa), si utilizza una firma di funzione. Di seguito, la funzione sayHello deve attenersi alla firma dopo i due punti:

// Dichiara la variabile sayHello, e le fornisce una firma che riceve una stringa e non ritorna nulla.
let sayHello: (name: string) => void;

// Definisce la funzione, soddisfacendo la sua firma
sayHello = (name) => {
  console.log('Hello ' + name);
};

sayHello('Danny'); // Hello Danny

Any - Un Tipo Dinamico

Usando il tipo any , possiamo praticamente considerare TypeScript come fosse JavaScript, in quanto qualsiasi tipo di valore sarà accettato:

let age: any = '100';
age = 100;
age = {
  years: 100,
  months: 2,
};

Si raccomanda per quanto possibile di evitare l'utilizzo del tipo any , in quanto impedisce a TypeScript di svolgere il proprio compito e può causare bug.

Alias di Tipo

Gli alias di tipo possono ridurre la duplicazione del codice, evitando che il proprio codice contenga parti ripetute. Di seguito possiamo vedere che l'alias di tipo PersonObject ha evitato una ripetizione, e agisce come unica fonte di verità per quello che un oggetto di tipo PersonObject dovrebbe contenere.

type StringOrNumber = string | number;

type PersonObject = {
  name: string;
  id: StringOrNumber;
};

const person1: PersonObject = {
  name: 'John',
  id: 1,
};

const person2: PersonObject = {
  name: 'Delia',
  id: 2,
};

const sayHello = (person: PersonObject) => {
  return 'Hi ' + person.name;
};

const sayGoodbye = (person: PersonObject) => {
  return 'Seeya ' + person.name;
};

Il DOM e la conversione di tipo

TypeScript non ha accesso al DOM come JavaScript. Ciò significa che ogni volta che proviamo ad accedere agli elementi DOM, TypeScript non è mai sicuro che esistano effettivamente.

L'esempio qui sotto evidenzia il problema:

const link = document.querySelector('a');

console.log(link.href); // ERRORE: Oggetto possibilmente 'null'. TypeScript non può essere sicuro che il tag anchor esista, visto che non può accedere al DOM

Con l'operatore di asserzione non-null (!) possiamo dire esplicitamente al compilatore che un'espressione ha un valore diverso da null o undefined. Questo può essere utile quando il compilatore non può dedurre il tipo con certezza, ma noi abbiamo più informazioni rispetto al compilatore.

// Qui stiamo dicendo al compilatore che siamo sicuri che questo tag anchor esiste
const link = document.querySelector('a')!;

console.log(link.href); // www.freeCodeCamp.org

Nota come non abbiamo dovuto dichiarare il tipo della variabile link. Questo perché TypeScript può vedere chiaramente (tramite la deduzione di tipo) che è di tipo HTMLAnchorElement.

Ma cosa succede se dovessimo selezionare un elemento DOM in base alla sua classe o id? TypeScript non può dedurre il tipo, poiché potrebbe essere qualsiasi cosa.

const form = document.getElementById('signup-form');

console.log(form.method);
// ERRORE: Oggetto possibilmente 'null'.
// ERRORE: La proprietà 'method' non esiste per il tipo 'HTMLElement'.

Qui sopra abbiamo ottenuto due errori. Dobbiamo dire a TypeScript che siamo sicuri che un certo form esiste, e che sappiamo che è di tipo HTMLFormElement. Lo facciamo con la conversione di tipo:

const form = document.getElementById('signup-form') as HTMLFormElement;

console.log(form.method); // post

Ora TypeScript è contento!

TypeScript ha anche un oggetto Event integrato. Quindi, se aggiungiamo un listener di eventi di invio al nostro form, TypeScript ci darà un errore se chiamiamo metodi che non fanno parte dell'oggetto Event. Guarda quanto è bello TypeScript – può dirci quando abbiamo commesso un errore di ortografia:

const form = document.getElementById('signup-form') as HTMLFormElement;

form.addEventListener('submit', (e: Event) => {
  e.preventDefault(); // impedisce alla pagina di aggiornarsi

  console.log(e.tarrget); // ERRORE: La proprietà 'tarrget' non esiste per il tipo 'Event'. Volevi dire 'target'?
});

Classi in TypeScript

Possiamo definire di quale tipo ogni dato dovrebbe essere in una classe:

class Person {
  name: string;
  isCool: boolean;
  pets: number;

  constructor(n: string, c: boolean, p: number) {
    this.name = n;
    this.isCool = c;
    this.pets = p;
  }

  sayHello() {
    return `Hi, my name is ${this.name} and I have ${this.pets} pets`;
  }
}

const person1 = new Person('Danny', false, 1);
const person2 = new Person('Sarah', 'yes', 6); // ERRORE: Argomento di tipo 'string' non è assegnabile al parametro di tipo 'boolean'.

console.log(person1.sayHello()); // Hi, my name is Danny and I have 1 pets

Ora potremo creare un array people che contiene solo oggetti istanziati dalla classe Person:

let People: Person[] = [person1, person2];

Possiamo aggiungere modificatori di accesso alle proprietà di una classe. TypeScript fornisce anche un nuovo modificatore di accesso chiamato readonly (sola lettura).

class Person {
  readonly name: string; // Questa proprietà è immutabile - può solo essere letta
  private isCool: boolean; // Si può accedere o modificare solo da metodi all'interno di questa classe
  protected email: string; // Si può accedere o modificare da questa classe e sue sottoclassi
  public pets: number; // Si può accedere o modificare da qualunque parte - compreso dall'esterno della classe

  constructor(n: string, c: boolean, e: string, p: number) {
    this.name = n;
    this.isCool = c;
    this.email = e;
    this.pets = p;
  }

  sayMyName() {
    console.log(`Your not Heisenberg, you're ${this.name}`);
  }
}

const person1 = new Person('Danny', false, 'dan@e.com', 1);
console.log(person1.name); // Fine
person1.name = 'James'; // Errore: di sola lettura
console.log(person1.isCool); // Errore: proprietà privata - accessibile solo all'interno della classe Person
console.log(person1.email); // Errore: proprietà privata - accessibile solo all'interno della classe Person e le sue sottoclassi
console.log(person1.pets); // Proprietà pubblica - nessun problema

Possiamo rendere il nostro codice più conciso scrivendo le proprietà della classe in questo modo:

class Person {
  constructor(
    readonly name: string,
    private isCool: boolean,
    protected email: string,
    public pets: number
  ) {}

  sayMyName() {
    console.log(`Your not Heisenberg, you're ${this.name}`);
  }
}

const person1 = new Person('Danny', false, 'dan@e.com', 1);
console.log(person1.name); // Danny

Scrivendo come indicato qui sopra, le proprietà sono assegnate automaticamente nel costruttore, risparmiandoci l'esplicita scrittura.

Nota che se omettiamo il modificatore di accesso, la modalità predefinita di accesso sarà public.

È possibile estendere le classi, proprio come nel normale JavaScript:

class Programmer extends Person {
  programmingLanguages: string[];

  constructor(
    name: string,
    isCool: boolean,
    email: string,
    pets: number,
    pL: string[]
  ) {
    // La chiamata a super deve fornire tutti i parametri per il costruttore base (quello della classe Person), in quanto il costruttore non viene ereditato.
    super(name, isCool, email, pets);
    this.programmingLanguages = pL;
  }
}

Per maggiori informazioni sulle classi, fai riferimento alla documentazione ufficiale di TypeScript.

Moduli in TypeScript

In JavaScript, un modulo è solo un file contenente codice correlato. Le funzionalità possono essere importate ed esportate tra moduli, mantenendo il codice ben organizzato.

Anche TypeScript supporta i moduli. I file TypeScript verranno compilati in più file JavaScript

Nel file tsconfig.json, modifica le seguenti opzioni per supportare la moderna importazione ed esportazione:

 "target": "es2016",
 "module": "es2015"

(Per progetti Node probabilmente preferirai  "module": "CommonJS" visto che Node non supporta ancora la moderna importazione ed esportazione)

Ora, nel tuo file HTML, modifica l'attributo type del tag script con il valore module:

<script type="module" src="/public/script.js"></script>

Ora puoi usare la sintassi di importazione/esportazione ES6:

// src/hello.ts
export function sayHi() {
  console.log('Hello there!');
}

// src/script.ts
import { sayHi } from './hello.js';

sayHi(); // Hello there!

Nota: Importa sempre il file JavaScript, anche nei file TypeScript.

Interfacce in TypeScript

Le interfacce definiscono l'aspetto di un oggetto:

interface Person {
  name: string;
  age: number;
}

function sayHi(person: Person) {
  console.log(`Hi ${person.name}`);
}

sayHi({
  name: 'John',
  age: 48,
}); // Hi John

Puoi anche definire un tipo di oggetto usando un alias di tipo:

type Person = {
  name: string;
  age: number;
};

function sayHi(person: Person) {
  console.log(`Hi ${person.name}`);
}

sayHi({
  name: 'John',
  age: 48,
}); // Hi John

Oppure un oggetto potrebbe essere definito in modo anonimo:

function sayHi(person: { name: string; age: number }) {
  console.log(`Hi ${person.name}`);
}

sayHi({
  name: 'John',
  age: 48,
}); // Hi John

Le interfacce sono molto simili agli alias di tipo e in molti casi sono intercambiabili. La distinzione chiave è che gli alias di tipo non possono essere riaperti per aggiungere nuove proprietà, rispetto a un'interfaccia che è sempre estendibile.

I seguenti esempi sono tratti dalla documentazione di TypeScript.

Estendere un'interfaccia:

interface Animal {
  name: string
}

interface Bear extends Animal {
  honey: boolean
}

const bear: Bear = {
  name: "Winnie",
  honey: true,
}

Estendere un tipo tramite intersezioni:

type Animal = {
  name: string
}

type Bear = Animal & {
  honey: boolean
}

const bear: Bear = {
  name: "Winnie",
  honey: true,
}

Aggiungere nuovi campi a una interfaccia esistente:

interface Animal {
  name: string
}

// Riapertura dell'interfaccia Animal per aggiungere un nuovo campo
interface Animal {
  tail: boolean
}

const dog: Animal = {
  name: "Bruce",
  tail: true,
}

Ecco la differenza chiave: un tipo non può essere mutato dopo la sua creazione:

type Animal = {
  name: string
}

type Animal = {
  tail: boolean
}
// ERRORE: Identificatore duplicato 'Animal'.

Come regola pratica, la documentazione TypeScript consiglia di utilizzare le interfacce per definire gli oggetti, finché non è necessario utilizzare le funzionalità di un tipo.

Le interfacce possono anche definire le firme della funzione:

interface Person {
  name: string
  age: number
  speak(sentence: string): void
}

const person1: Person = {
  name: "John",
  age: 48,
  speak: sentence => console.log(sentence),
}

Potresti chiederti perché dovremmo usare una interfaccia al posto di una classe nell'esempio qui sopra.

Uno dei vantaggi nell'utilizzo di una interfaccia è che viene usata solo da TypeScript e non da JavaScript. Ciò significa che non verrà compilata, quindi non aumenterà la dimensione del tuo codice JavaScript. Le classi sono funzionalità di JavaScript, quindi verrebbero compilate.

Inoltre, una classe è essenzialmente una fabbrica di oggetti (vale a dire uno stampo di quello che dovrebbe essere un oggetto e quindi implementato), laddove un'interfaccia è una struttura usata solamente per la convalida di tipo.

Se una classe può avere proprietà inizializzate e metodi che aiutano a creare oggetti, un'interfaccia definisce essenzialmente le proprietà e il tipo che un oggetto può avere.

Interfacce con classi

Possiamo dire a una classe che deve contenere certe proprietà e metodi implementando un'interfaccia:

interface HasFormatter {
  format(): string;
}

class Person implements HasFormatter {
  constructor(public username: string, protected password: string) {}

  format() {
    return this.username.toLocaleLowerCase();
  }
}

// Devono essere oggetti che implementano l'interfaccia HasFormatter
let person1: HasFormatter;
let person2: HasFormatter;

person1 = new Person('Danny', 'password123');
person2 = new Person('Jane', 'TypeScripter1990');

console.log(person1.format()); // danny

Fa in modo che people sia un array di oggetti che implementano HasFormatter (verificando che ogni istanza di Person abbia un metodo format):

let people: HasFormatter[] = [];
people.push(person1);
people.push(person2);

Tipi Letterali in TypeScript

Oltre ai tipi generali string e number, possiamo fare riferimento a specifiche stringhe o numeri nella definizione del tipo:

// Tipo union con un tipo letterale in ciascuna posizione
let favouriteColor: 'red' | 'blue' | 'green' | 'yellow';

favouriteColor = 'blue';
favouriteColor = 'crimson'; // ERRORE: Il tipo '"crimson"' non è assegnabile al tipo '"red" | "blue" | "green" | "yellow"'.

Generici

I generici consentono di creare un componente che può funzionare su una varietà di tipi, invece che su uno singolo, il che aiuta a rendere il componente più riutilizzabile.

Facciamo un esempio per mostrare cosa significa...

La funzione addID accetta qualsiasi oggetto, e ritorna un nuovo oggetto con tutte le proprietà e valori dell'oggetto ricevuto, con in più un proprietà id che contiene un valore casuale tra 0 e 1000. In breve fornisce un identificativo a ogni oggetto.

const addID = (obj: object) => {
  let id = Math.floor(Math.random() * 1000);

  return { ...obj, id };
};

let person1 = addID({ name: 'John', age: 40 });

console.log(person1.id); // 271
console.log(person1.name); // ERRORE: La proprietà 'name' non esiste per il tipo '{ id: number; }'.

Come puoi vedere,  TypeScript dà un errore quando cerchiamo di accedere alla proprietà name. Questo perché quando passiamo un oggetto ad addID, non stiamo specificando quali proprietà questo oggetto dovrebbe avere, quindi TypeScript non ha idea di quali proprietà sia dotato l'oggetto (non le ha "catturate"). Quindi l'unica proprietà che conosce TypeScript sull'oggetto da restituire è id.

Come possiamo quindi passare un qualsiasi oggetto ad addID, e nel contempo dire a TypeScript quali proprietà e valori ha l'oggetto? Possiamo usare un  generico, <T>, dove T è noto come parametro tipo.

// <T> è solo una convenzione - per esempio potremmo usare <X> o <A>
const addID = <T>(obj: T) => {
  let id = Math.floor(Math.random() * 1000);

  return { ...obj, id };
};

Cosa fa? Ora quando passiamo un oggetto a addID, diciamo a TypeScript di catturare il tipo, di conseguenza T diventa qualsiasi tipo gli passiamo. Ora addID saprà quali proprietà sono sull'oggetto che riceve.

Ora però abbiamo un problema, qualsiasi cosa può essere passata ad addID e TypeScript catturerà il tipo e non riscontrerà problemi:

let person1 = addID({ name: 'John', age: 40 });
let person2 = addID('Sally'); // Passa una stringa - nessun problema

console.log(person1.id); // 271
console.log(person1.name); // John

console.log(person2.id);
console.log(person2.name); // ERRORE: La proprietà 'name' non esiste per il tipo '"Sally" & { id: number; }'.

Quando abbiamo passato una stringa, TypeScript non ha riscontrato problemi. Ha rilevato un errore solo quando abbiamo tentato di accedere alla proprietà name. Quindi ci serve un vincolo: dobbiamo dire a TypeScript che dovrebbero essere accettati solo oggetti rendendo il nostro tipo generico, T, un'estensione di object:

const addID = <T extends object>(obj: T) => {
  let id = Math.floor(Math.random() * 1000);

  return { ...obj, id };
};

let person1 = addID({ name: 'John', age: 40 });
let person2 = addID('Sally'); // ERRORE: L'argumento di tipo 'string' non è assegnabile al parametro di tipo 'object'.

L'errore è rilevato immediatamente, perfetto... beh, non proprio. In JavaScript, gli array sono oggetti, quindi possiamo vanificare il controllo passando un array:

let person2 = addID(['Sally', 26]); // Passo un array - nessun problema

console.log(person2.id); // 824
console.log(person2.name); // Errore: La proprietà  'name' non esiste per il tipo '(string | number)[] & { id: number; }'.

Potremmo risolverlo dicendo che l'argomento oggetto dovrebbe avere una proprietà name con valore stringa:

const addID = <T extends { name: string }>(obj: T) => {
  let id = Math.floor(Math.random() * 1000);

  return { ...obj, id };
};

let person2 = addID(['Sally', 26]); // ERRORE: l'argomento dovrebbe avere una proprietà 'name' con valore stringa

Il tipo può anche essere passato a  <T>, come sotto, ma per la maggior parte delle volte non è necessario, in quanto TypeScript lo può dedurre.

// Qui sotto abbiamo esplicitamente dichiarato che di quale tipo dovrebbe essere l'argomento tra parentesi angolate.
let person1 = addID<{ name: string; age: number }>({ name: 'John', age: 40 });

I generici ti consentono di avere la sicurezza dei tipi nei componenti in cui gli argomenti e i tipi restituiti non sono noti in anticipo.

In TypeScript, i generici sono usati quando vogliamo descrivere una corrispondenza tra due valori. Nell'esempio qui sopra il tipo da ritornare è stato correlato al tipo in input. Abbiamo usato un generico per descrivere la corrispondenza.

Un altro esempio: se ci serve una funzione che accetta tipi multipli, è meglio usare un generico piuttosto che il tipo any. Qui sotto è mostrato quale problema si avrebbe usando any:

function logLength(a: any) {
  console.log(a.length); // Nessun errore
  return a;
}

let hello = 'Hello world';
logLength(hello); // 11

let howMany = 8;
logLength(howMany); // undefined (ma nessun errore TypeScript - sicuramente vogliamo che TypeScript ci dica che stiamo tentando di accedere alla proprietà length su di un numero!)

Potremmo provare a usare un generico:

function logLength<T>(a: T) {
  console.log(a.length); // ERRORE: TypeScript non è certo che `a` sia un valore che abbia una proprietà length
  return a;
}

Almeno ora stiamo ricevendo un riscontro che possiamo usare per irrobustire il nostro codice.

Soluzione: usa un generico che estenda un'interfaccia che assicuri che ogni argomento passato abbia una proprietà length:

interface hasLength {
  length: number;
}

function logLength<T extends hasLength>(a: T) {
  console.log(a.length);
  return a;
}

let hello = 'Hello world';
logLength(hello); // 11

let howMany = 8;
logLength(howMany); // Errore: i numeri non hanno la proprietà length

Possiamo anche scrivere una funzione dove l'argomento è un array di elementi che hanno tutti una proprietà  length:

interface hasLength {
  length: number;
}

function logLengths<T extends hasLength>(a: T[]) {
  a.forEach((element) => {
    console.log(element.length);
  });
}

let arr = [
  'This string has a length prop',
  ['This', 'arr', 'has', 'length'],
  { material: 'plastic', length: 30 },
];

logLengths(arr);
// 29
// 4
// 30

I generici sono una funzionalità fantastica di TypeScript!

Generici con interfacce

Quando non sappiamo in anticipo di quale tipo sarà  un determinato valore in un oggetto, possiamo usare un generico per passare il tipo:

// Il tipo, T, sarà passato
interface Person<T> {
  name: string;
  age: number;
  documents: T;
}

// Dobbiamo passare il tipo `documents` - un array di stringhe in questo caso
const person1: Person<string[]> = {
  name: 'John',
  age: 48,
  documents: ['passport', 'bank statement', 'visa'],
};

// Implementiamo ancora una interfaccia `Person` e passiamo il tipo per 'documents' - in questo caso una stringa
const person2: Person<string> = {
  name: 'Delia',
  age: 46,
  documents: 'passport, P45',
};

Enumerazioni in TypeScript

Le enumerazioni sono una caratteristica speciale che TypeScript porta a JavaScript. Le enumerazioni ci consentono di definire o dichiarare una raccolta di valori correlati, che possono essere numeri o stringhe, come un insieme di costanti denominate.

enum ResourceType {
  BOOK,
  AUTHOR,
  FILM,
  DIRECTOR,
  PERSON,
}

console.log(ResourceType.BOOK); // 0
console.log(ResourceType.AUTHOR); // 1

// Per partire da 1
enum ResourceType {
  BOOK = 1,
  AUTHOR,
  FILM,
  DIRECTOR,
  PERSON,
}

console.log(ResourceType.BOOK); // 1
console.log(ResourceType.AUTHOR); // 2

Per impostazione predefinita, le enumerazioni sono basate su numeri: memorizzano i valori delle stringhe come numeri. Ma possono anche essere stringhe:

enum Direction {
  Up = 'Up',
  Right = 'Right',
  Down = 'Down',
  Left = 'Left',
}

console.log(Direction.Right); // Right
console.log(Direction.Down); // Down

Le enumerazioni sono utili quando abbiamo un insieme di costanti correlate. Ad esempio, invece di utilizzare numeri non descrittivi in ​​tutto il codice, le enumerazioni rendono il codice più leggibile con costanti descrittive.

Le enumerazioni possono anche prevenire i bug, poiché quando si digita il nome dell'enumerazione, l'intellisense del tuo IDE apparirà e ti darà l'elenco delle possibili opzioni che possono essere selezionate.

La modalità "strict" (rigorosa) di TypeScript

Si consiglia di abilitare tutte le operazioni di controllo di tipo rigoroso nel file tsconfig.json. Ciò farà sì che TypeScript riporti più errori, ma aiuterà a prevenire che molti bug si insinuino nell'applicazione.

 // tsconfig.json
 "strict": true

Discutiamo di un paio di cose che fa la modalità strict: nessun implicito any e controlli di null rigorosi.

Nessun any implicito

Nella funzione qui sotto, TypeScript ha dedotto che il parametro  a è di tipo any. Come puoi vedere, quando passiamo un numero a questa funzione e cerchiamo di stampare la proprietà name non viene riportato alcun errore. Non va bene.

function logName(a) {
  // Nessun errore??
  console.log(a.name);
}

logName(97);

Con l'opzione noImplicitAny attivata, TypeScript segnalerà immediatamente un errore se non dichiariamo esplicitamente il tipo di a:

// ERRORE: Il parametro 'a' ha un tipo 'any' implicito.
function logName(a) {
  console.log(a.name);
}

Verifiche rigorose di null

Quando l'opzione strictNullChecks è false, TypeScript in effetti ignora null e undefined. Questo può portare a errori inattesi in fase di esecuzione.

Con strictNullChecks impostato su true, null e undefined hanno i loro propri tipi, e otterrai un errore di tipo se li assegni a una variabile che si attende un valore concreto (per esempio string).

let whoSangThis: string = getSong();

const singles = [
  { song: 'touch of grey', artist: 'grateful dead' },
  { song: 'paint it black', artist: 'rolling stones' },
];

const single = singles.find((s) => s.song === whoSangThis);

console.log(single.artist);

Qui sopra singles.find non dà garanzie che trovi un oggetto con la proprietà song uguale al parametro passato s, tuttavia abbiamo scritto il codice come se lo trovasse sempre.

Impostando strictNullChecks su true, TypeScript darà un errore in quanto non abbiamo garantito che single esista prima di provare a usarlo:

const getSong = () => {
  return 'song';
};

let whoSangThis: string = getSong();

const singles = [
  { song: 'touch of grey', artist: 'grateful dead' },
  { song: 'paint it black', artist: 'rolling stones' },
];

const single = singles.find((s) => s.song === whoSangThis);

console.log(single.artist); // ERRORE: è possibile che l'oggetto possa essere 'undefined'.

In pratica TypeScript ci sta dicendo che dobbiamo assicurarci che single esista prima di usarlo. Dobbiamo controllare prima se non sia null o undefined:

if (single) {
  console.log(single.artist); // rolling stones
}

Restringimento in TypeScript

In un programma TypeScript, il tipo di una variabile può essere modificato da meno preciso a più preciso. Questo processo viene chiamato restringimento del tipo.

Ecco un esempio che mostra come TypeScript restringe i tipi meno specifici  string | number in tipi più specifici quando usiamo costrutti if con typeof:

function addAnother(val: string | number) {
  if (typeof val === 'string') {
    // TypeScript considera `val` come una stringa in questo blocco, quindo possiamo usare i metodi di stringa su `val` e TypeScript non avrà da obiettare
    return val.concat(' ' + val);
  }

  // TypeScript sa che `val` qui è un numero
  return val + val;
}

console.log(addAnother('Woooo')); // Woooo Woooo
console.log(addAnother(20)); // 40

Un altro esempio: qui sotto abbiamo definito un tipo union chiamato allVehicles, che può essere sia di tipo Plane che Train.

interface Vehicle {
  topSpeed: number;
}

interface Train extends Vehicle {
  carriages: number;
}

interface Plane extends Vehicle {
  wingSpan: number;
}

type PlaneOrTrain = Plane | Train;

function getSpeedRatio(v: PlaneOrTrain) {
  // Qui vogliamo ritornare topSpeed/carriages, oppure topSpeed/wingSpan
  console.log(v.carriages); // ERRORE: 'carriages' non esiste per il tipo 'Plane'
}

Visto che la funzionegetSpeedRatio sta lavorando con tipi multipli, ci serve un modo per distinguere se v sia di tipo Plane oppure Train. Potremmo farlo dando a entrambi i tipi una proprietà distintiva comune, con un valore di stringa letterale:

// Tutti i tipi Train devono avere ora una proprietà type uguale a 'Train'
interface Train extends Vehicle {
  type: 'Train';
  carriages: number;
}

// Tutti i tipi Plane devono ora avere una proprietà type uguale a 'Plane'
interface Plane extends Vehicle {
  type: 'Plane';
  wingSpan: number;
}

type PlaneOrTrain = Plane | Train;

Ora noi, e TypeScript, possiamo restringere il tipo di v:

function getSpeedRatio(v: PlaneOrTrain) {
  if (v.type === 'Train') {
    // TypeScript ora sa che `v` è sicuramente un tipo `Train`. Ha ristretto il tipo da quello meno specifico `Plane | Train` a quello più specifico `Train`
    return v.topSpeed / v.carriages;
  }

  // Se il tipo non è Train, TypeScript restringe e deduce che  `v` deve essere un tipo Plane - intelligente!
  return v.topSpeed / v.wingSpan;
}

let bigTrain: Train = {
  type: 'Train',
  topSpeed: 100,
  carriages: 20,
};

console.log(getSpeedRatio(bigTrain)); // 5

Bonus: TypeScript con React

TypeScript supporta completamente React e JSX. Questo significa che possiamo usare TypeScript con i tre più comuni framework React:

  • create-react-app (impostazione TS)
  • Gatsby (impostazione TS)
  • Next.js (impostazione TS)

Se necessiti di una configurazione React-TypeScript più personalizzata, potresti impostare Webpack (un unificatore di moduli) e configurare personalmente il file tsconfig.json. La maggior parte delle volte, tuttavia, un framework farà il lavoro.

Per impostare create-react-app con TypeScript, per esempio, esegui semplicemente:

npx create-react-app my-app --template typescript

# oppure

yarn create react-app my-app --template typescript

Nella cartella src, ora possiamo creare file con suffisso .ts (per normali file TypeScript) oppure .tsx (per TypeScript con React) e scrivere i nostri componenti con TypeScript. Il codice successivamente compilato in JavaScript si troverà nella cartella public.

Prop di React con TypeScript

Qui sotto stiamo dichiarando che Person dovrebbe essere un componente React funzionale che accetta un oggetto props con una prop chiamata name, che dovrebbe essere una stringa, e age, che dovrebbe essere un numero.

// src/components/Person.tsx
import React from 'react';

const Person: React.FC<{
  name: string;
  age: number;
}> = ({ name, age }) => {
  return (
    <div>
      <div>{name}</div>
      <div>{age}</div>
    </div>
  );
};

export default Person;

Tuttavia, la maggior parte degli sviluppatori preferisce usare un'interfaccia per specificare i tipi di prop:

interface Props {
  name: string;
  age: number;
}

const Person: React.FC<Props> = ({ name, age }) => {
  return (
    <div>
      <div>{name}</div>
      <div>{age}</div>
    </div>
  );
};

Successivamente possiamo importare questo componente in App.tsx. Se non forniamo le prop del tipo specificato, TypeScript genererà un errore.

import React from 'react';
import Person from './components/Person';

const App: React.FC = () => {
  return (
    <div>
      <Person name='John' age={48} />
    </div>
  );
};

export default App;

Ecco alcuni esempi di ciò che potremmo avere come tipi di props:

interface PersonInfo {
  name: string;
  age: number;
}

interface Props {
  text: string;
  id: number;
  isVeryNice?: boolean;
  func: (name: string) => string;
  personInfo: PersonInfo;
}

Gli hook di React con TypeScript

useState()

Possiamo dichiarare di quali tipi dovrebbe essere una variabile di stato usando le parentesi angolate. Qui sotto, se omettessimo le parentesi angolate, TypeScript dedurrebbe che cash è un numero. Quindi, se vogliamo che sia anche null, dobbiamo specificare:

const Person: React.FC<Props> = ({ name, age }) => {
  const [cash, setCash] = useState<number | null>(1);

  setCash(null);

  return (
    <div>
      <div>{name}</div>
      <div>{age}</div>
    </div>
  );
};

useRef()

useRef restituisce un oggetto mutabile che persiste per tutto il ciclo di vita del componente. Possiamo dire a TypeScript a cosa dovrebbe fare riferimento l'oggetto ref – di seguito diciamo che la prop dovrebbe essere un tipo HTMLInputElement:

const Person: React.FC = () => {
  // Initialise .current property to null
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <div>
      <input type='text' ref={inputRef} />
    </div>
  );
};

Per ulteriori informazioni circa React con TypeScript dai un'occhiata a questi eccezionali cheatsheet per React-TypeScript.

Risorse utili e ulteriori letture

(Le risorse linkate sono in lingua originale inglese).

Grazie per aver letto!

Spero che questo articolo ti sia stato utile. Se sei arrivato qui, ora conosci i fondamenti principali di TypeScript e puoi iniziare a usarlo nei tuoi progetti.

Ancora una volta, puoi anche scaricare il mio cheat sheet di TypeScript di una pagina in PDF oppure ordinare un poster fisico.

Per saperne di più su di me, puoi trovarmi su Twitter e YouTube.

Saluti!