Menu

Ereditarietà in TypeScript: extends, super e override

L'ereditarietà delle classi in TypeScript: extends e super, override dei metodi con tipi compatibili, la parola chiave override e noImplicitOverride, i membri protected, come ridichiarare campi con declare e quando implements è lo strumento migliore.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Una classe TypeScript eredita da un'altra con extends. La sottoclasse riceve tutti i campi e i metodi del genitore, chiama il costruttore del genitore con super(...) e può fare override dei metodi, e TypeScript controlla che ogni override resti compatibile con i tipi del genitore.

È l'ereditarietà delle classi di JavaScript (vedi ereditarietà in JavaScript per la catena dei prototipi che c'è dietro). TypeScript aggiunge i controlli di tipo: un Dog è assegnabile ovunque sia atteso un Animal, e il compilatore verifica l'override e la chiamata a super.

Override dei metodi

Un metodo della sottoclasse con lo stesso nome sostituisce quello del genitore. Il suo tipo deve essere assegnabile a quello del genitore: parametri uguali o compatibili, tipo di ritorno uguale o più stretto. Altrimenti qualsiasi codice che ha in mano un Animal potrebbe chiamare il metodo con argomenti che la sottoclasse non sa gestire.

Cambiare il parametro in value: string darebbe l'errore TS2416: Property 'format' in type 'CurrencyFormatter' is not assignable to the same property in base type 'Formatter'. Il ciclo mostra perché: chiama format con un numero su ogni elemento, senza sapere quale sottoclasse ha davanti.

C'è una falla. I parametri dei metodi vengono confrontati in modo bivariante, quindi un override che accetta un tipo più stretto, come format(value: 1 | 2), compila anche se il ciclo potrebbe comunque passargli 1234567. I tipi di ritorno invece vengono controllati in modo rigoroso: restituire number invece di string è TS2416.

La parola chiave override e noImplicitOverride

override dice "questo membro sostituisce uno della classe base", e il compilatore ti prende in parola. Se la base non ha quel membro (un refuso, o un metodo che qualcuno ha rinominato nel genitore), è un errore:

index.ts(9,14): error TS4117: This member cannot have an 'override' modifier because it is not declared in the base class 'Animal'. Did you mean 'speak'?

Senza override, quel refuso compila e speak() restituisce "..." senza avvisare. La parola chiave aiuta solo dove è scritta, quindi attiva noImplicitOverride in tsconfig.json. Allora ogni metodo che fa override di un membro della base deve dichiararlo, e un semplice speak() in una sottoclasse dà l'errore TS4114 (This member must have an 'override' modifier because it overrides a member in the base class 'Animal').

{
    "compilerOptions": {
        "strict": true,
        "noImplicitOverride": true
    }
}

noImplicitOverride non fa parte di strict; devi aggiungerlo tu. override viene cancellato dal JavaScript compilato.

Membri protected nelle sottoclassi

I membri private del genitore sono invisibili nella sottoclasse. I membri protected sono visibili, ed è proprio per questo che protected esiste: è l'API che una classe genitore offre alle sue figlie.

Vedi modificatori di accesso per le regole complete, compreso il motivo per cui né private né protected vengono applicati a runtime.

Ridichiarare un campo con un tipo più stretto

Spesso una sottoclasse sa che un campo contiene qualcosa di più specifico di quanto dica il genitore. Ridichiararlo come pet: Dog ha un effetto a runtime con i class field moderni: il campo della sottoclasse viene inizializzato a undefined dopo il ritorno di super(), cancellando ciò che il costruttore del genitore aveva salvato. A meno che il costruttore della sottoclasse non lo assegni di nuovo, TypeScript lo segnala come TS2612 (Property 'pet' will overwrite the base property in 'Home'). Usa declare per cambiare solo il tipo:

declare non produce nulla, quindi il valore impostato dal costruttore di Home sopravvive. È il costruttore che accetta solo un Dog a rendere vero il tipo più stretto.

implements o extends

Le due parole chiave rispondono a domande diverse, e una classe può usarle entrambe.

extendsimplements
Funziona conuna sola classeun numero qualsiasi di interfacce (o alias di tipi oggetto)
Porta codicesì: campi, metodi, costruttoreno, controlla solo la forma
Esiste a runtimesì, la catena dei prototipino, viene cancellato
instanceof funzionasìno
Uso tipicocondividere comportamento realepromettere una forma al resto del codice

Una classe può estendere una sola classe (TS1174: Classes can only extend a single class). Per combinare comportamenti da più punti, implementa più interfacce e condividi il codice tramite composizione (un campo che contiene un oggetto helper) o mixin.

Mixin

Un mixin è una funzione che riceve una classe e ne restituisce una sottoclasse. Offre una forma di ereditarietà multipla che supera il controllo dei tipi:

L'any[] in Constructor è obbligatorio: TypeScript accetta come base di un mixin solo una classe il cui costruttore prende un unico parametro rest di tipo any[]. I mixin funzionano ma rendono più difficili da leggere i tipi e gli stack trace, quindi prova prima la composizione.

Errori comuni

  • Chiamare un metodo sovrascrivibile dal costruttore della base. Il costruttore della base viene eseguito prima che i campi della sottoclasse siano inizializzati, quindi un override che legge this.items vede undefined, e una chiamata come this.items.push(x) lancia un TypeError a runtime. TypeScript non lo intercetta.
  • Dimenticare super(...) o chiamarlo tardi. Errori TS2377 e TS17009; il genitore deve costruire l'oggetto prima che la sottoclasse tocchi this.
  • Ridichiarare un campo del genitore senza declare. Il campo della sottoclasse lo riporta a undefined.
  • Gerarchie profonde. Ogni livello lega la figlia ai dettagli interni del genitore. Due livelli di solito bastano; oltre, preferisci la composizione o le interfacce.

Domande frequenti

Come funziona l'ereditarietà in TypeScript?

Una classe eredita da un'altra con extends: class Dog extends Animal. La sottoclasse riceve i campi e i metodi del genitore, chiama il costruttore del genitore con super(...) e può fare override dei metodi. TypeScript controlla che gli override restino compatibili con i tipi del genitore.

Cosa fa la parola chiave override in TypeScript?

override marca un metodo o una proprietà che sostituisce un membro della classe base. Se la classe base non ha un membro con quel nome, il compilatore segnala TS4117, e così intercetta refusi e membri rinominati nel genitore. Con noImplicitOverride attivo, ogni override deve avere la parola chiave (altrimenti TS4114).

Come si chiama un metodo della classe padre in TypeScript?

Usa super.methodName(...) dentro il metodo della sottoclasse. Nel costruttore, super(...) chiama il costruttore del genitore e deve essere eseguito prima di qualsiasi uso di this.

TypeScript supporta l'ereditarietà multipla?

No. Una classe può estendere una sola classe (TS1174: Classes can only extend a single class). Può implementare un numero qualsiasi di interfacce, e i mixin (funzioni che ricevono una classe e ne restituiscono una sottoclasse) combinano comportamenti da più fonti.

Che differenza c'è tra extends e implements?

extends eredita codice reale da una classe genitore. implements controlla solo che una classe rispetti la forma di un'interfaccia e non aggiunge nulla a runtime. Una classe può fare entrambe le cose: class Dog extends Animal implements Pet.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA