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.
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:
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.
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:
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
- The official TypeScript docs
- The Net Ninja's TypeScript video series (eccezionale!)
- Ben Awad's TypeScript with React video
- Narrowing in TypeScript (una caratteristica molto interessante di TS che dovresti imparare)
- Function overloads
- Primitive values in JavaScript
- JavaScript objects
(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!