Una classe TypeScript è una classe JavaScript con i tipi: ogni campo viene dichiarato con il suo tipo nel corpo della classe, e metodi e costruttore ricevono parametri e valori di ritorno tipizzati.
L'output compilato è una normale classe JavaScript senza i tipi, quindi tutto ciò che vale per le classi JavaScript resta valido. Ciò che aggiunge TypeScript avviene in compilazione: l'ultima chiamata viene rifiutata prima che il programma venga eseguito.
Dichiarare i campi
Un campo ha bisogno di una dichiarazione nel corpo della classe prima che il costruttore possa assegnarlo. Assegnare this.owner senza dichiarare owner è l'errore TS2339 (la proprietà non esiste). Un campo con un inizializzatore riceve il tipo dedotto dal valore, proprio come una variabile.
| Dichiarazione | Significato |
|---|---|
name: string; | Va assegnato nel costruttore |
count = 0; | Inizializzato, tipo dedotto come number |
label?: string; | Facoltativo, il tipo è string | undefined |
readonly id: number; | Assegnato una volta, poi solo lettura |
data!: string[]; | Definite assignment: fidati, verrà impostato |
static total = 0; | Appartiene alla classe, non alle istanze |
Con strict attivo, strictPropertyInitialization verifica che ogni campo non facoltativo riceva un valore. Questa classe se ne dimentica due:
Il compilatore li segnala entrambi:
index.ts(3,5): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.
index.ts(4,5): error TS2564: Property 'age' has no initializer and is not definitely assigned in the constructor.
Il controllo esiste perché senza di esso new Profile().name.toUpperCase() supererebbe il controllo dei tipi e poi andrebbe in crash con undefined. Sistema ogni campo in uno di quattro modi: dagli un inizializzatore, assegnalo nel costruttore, rendilo facoltativo con ?, oppure scrivi name!: string quando un framework o un metodo init() lo imposta più tardi. L'ultimo disattiva il controllo per quel campo, quindi usalo con parsimonia.
Metodi e this
I metodi si tipizzano come le funzioni. Dentro un metodo, this ha il tipo dell'istanza. Un metodo può restituire this per permettere il concatenamento, e il tipo di ritorno segue automaticamente le sottoclassi.
class QueryBuilder {
private parts: string[] = [];
where(condition: string): this {
this.parts.push(condition);
return this;
}
build(): string {
return this.parts.join(" AND ");
}
}
const sql = new QueryBuilder().where("age > 18").where("active = 1").build();
console.log(sql); // age > 18 AND active = 1
Un metodo perde il suo this quando lo stacchi dall'oggetto, un classico bug di JavaScript. TypeScript non lo intercetta di default, ma lo fa se il metodo dichiara un parametro this. Quel parametro viene cancellato dall'output e protegge solo il punto della chiamata:
Il campo arrow funziona perché ogni istanza riceve la propria funzione che cattura this. Il costo è una funzione per istanza invece di un unico metodo condiviso sul prototipo.
Getter e setter
Gli accessor get e set sembrano proprietà dall'esterno. Un getter senza setter è di sola lettura: assegnargli un valore è un errore di compilazione (TS2540).
Il compilatore rifiuta l'assegnazione, e se il controllo viene aggirato anche il runtime lancia un TypeError, perché il file compilato gira in strict mode (il compilatore emette "use strict" in cima). Il RangeError nel setter è un controllo a runtime che hai scritto tu. Il tipo number dice solo che il valore è un numero; non sa nulla di intervalli.
Membri statici
I campi e i metodi static appartengono alla classe stessa. Li raggiungi tramite il nome della classe, non tramite un'istanza. Un blocco static { } viene eseguito una volta quando la classe viene definita.
I membri statici non possono usare i parametri di tipo della classe: in class Box<T>, un static empty: T è l'errore TS2302, perché esiste un solo campo statico, condiviso da ogni Box<string>, Box<number> e così via.
Implementare un'interfaccia
implements chiede al compilatore di verificare che la classe abbia tutto ciò che un'interfaccia richiede. Un membro mancante è l'errore TS2420: Class 'X' incorrectly implements interface 'Y'.
Una classe può implementare più interfacce: class Doc implements Printable, Serializable. Ci sono due cose che implements non fa. Non tipizza al posto tuo i parametri dei metodi della classe: check(s) { ... } dentro una classe che implementa check(s: string): boolean è comunque l'errore TS7006 (Parameter 's' implicitly has an 'any' type), quindi annotali. E non lascia traccia a runtime, quindi obj instanceof Shape non è possibile.
Il private radius nel costruttore è una parameter property: dichiara e assegna il campo in un solo passaggio. Vedi costruttori.
Una classe è anche un tipo
Una dichiarazione di classe crea due cose con un solo nome: un valore (la funzione costruttrice che chiami con new) e un tipo (la forma di un'istanza). Dato che TypeScript confronta i tipi per struttura, qualsiasi oggetto con gli stessi membri pubblici si adatta al tipo, anche uno non creato dalla classe.
L'oggetto semplice supera il controllo dei tipi ma a runtime non è un Point: ({ x: 6, y: 8 }) instanceof Point è false. Una classe con un membro private o #private lo impedisce: solo le istanze di quella classe (o delle sue sottoclassi) le sono assegnabili.
Errori comuni
- Dichiarare un campo e non assegnarlo mai.
TS2564ti sta dicendo che il campo sarebbeundefined. Inizializzalo invece di zittirlo con!. - Passare un metodo come callback.
button.onclick = obj.handleperdethis. Usa un campo arrow function oobj.handle.bind(obj). - Aspettarsi che
implementsaggiunga codice o tipi. Controlla soltanto. I tipi dei parametri vanno comunque annotati. - Usare
instanceofcon un'interfaccia. Le interfacce non esistono a runtime; controlla una classe o usa una type guard. - Pensare che i tipi validino i dati. Un campo
numberaccetta qualsiasi numero in compilazione, e qualsiasi cosa da dati non tipizzati a runtime.
Domande frequenti
Come si crea una classe in TypeScript?
Dichiara ogni campo con il suo tipo nel corpo della classe, poi scrivi il costruttore e i metodi come in JavaScript: class User { name: string; constructor(name: string) { this.name = name; } greet(): string { return "Hi, " + this.name; } }. Crea un'istanza con new User("Ada").
Cosa significa "has no initializer and is not definitely assigned in the constructor"?
È l'errore TS2564 di strictPropertyInitialization (parte di strict). Un campo tipizzato string partirebbe come undefined perché nulla lo imposta. Risolvi con un inizializzatore (name = ""), un'assegnazione nel costruttore, un campo facoltativo (name?: string) oppure, quando qualcosa fuori dal costruttore lo imposta, una definite assignment assertion (name!: string).
Che differenza c'è tra implements ed extends in TypeScript?
extends eredita codice da una classe genitore: i suoi campi e metodi esistono sulla figlia a runtime. implements chiede solo al compilatore di verificare che la classe abbia la forma di un'interfaccia. Non aggiunge nulla alla classe e sparisce dal JavaScript compilato.
Una classe si può usare come tipo in TypeScript?
Sì. Il nome di una classe è sia un valore (il costruttore) sia un tipo (la forma delle sue istanze). let u: User accetta qualsiasi oggetto con gli stessi membri pubblici, perché TypeScript confronta i tipi in modo strutturale. typeof User è il tipo del costruttore stesso.
Le classi TypeScript sono diverse da quelle JavaScript a runtime?
No. TypeScript compila una classe in una normale classe JavaScript. Le annotazioni di tipo, le clausole implements e i modificatori come private vengono cancellati; a runtime esistono solo le funzionalità JavaScript come i campi #private, i blocchi static e i getter.