Déclaration d'une méthode abstraite dans TypeScript

195

J'essaie de comprendre comment définir correctement les méthodes abstraites en TypeScript:

En utilisant l'exemple d'héritage d'origine:

class Animal {
    constructor(public name) { }
    makeSound(input : string) : string;
    move(meters) {
        alert(this.name + " moved " + meters + "m.");
    }
}

class Snake extends Animal {
    constructor(name) { super(name); }
    makeSound(input : string) : string {
        return "sssss"+input;
    }
    move() {
        alert("Slithering...");
        super.move(5);
    }
}

Je voudrais savoir comment définir correctement la méthode makeSound, donc elle est typée et possible de trop.

De plus, je ne sais pas comment définir correctement les protectedméthodes - cela semble être un mot-clé, mais n'a aucun effet et le code ne se compilera pas.

Vojtěch
la source
4
Les classes et méthodes abstraites sont désormais une nouvelle fonctionnalité du prochain TypeScript 1.6.
falconepl

Réponses:

284

La namepropriété est marquée comme protected. Cela a été ajouté dans TypeScript 1.3 et est maintenant fermement établi.

La makeSoundméthode est marquée comme abstract, tout comme la classe. Vous ne pouvez pas instancier directement un Animalmaintenant, car il est abstrait. Cela fait partie de TypeScript 1.6 , qui est maintenant officiellement en ligne.

abstract class Animal {
    constructor(protected name: string) { }

    abstract makeSound(input : string) : string;

    move(meters) {
        alert(this.name + " moved " + meters + "m.");
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }

    makeSound(input : string) : string {
        return "sssss"+input;
    }

    move() {
        alert("Slithering...");
        super.move(5);
    }
}

L'ancienne façon d'imiter une méthode abstraite était de lancer une erreur si quelqu'un l'utilisait. Vous ne devriez plus avoir à faire cela une fois que TypeScript 1.6 atterrit dans votre projet:

class Animal {
    constructor(public name) { }
    makeSound(input : string) : string {
        throw new Error('This method is abstract');
    }
    move(meters) {
        alert(this.name + " moved " + meters + "m.");
    }
}

class Snake extends Animal {
    constructor(name) { super(name); }
    makeSound(input : string) : string {
        return "sssss"+input;
    }
    move() {
        alert("Slithering...");
        super.move(5);
    }
}
Fenton
la source
Est-ce un comportement normal que le compilateur ne se plaint pas si je manque un paramètre, change un type de paramètre ou change le type de retour lors de la substitution d'une méthode abstraite?
Vetterjack
1
Il est possible d'omettre un paramètre (si vous ne l'utilisez pas, vous pouvez ignorer toute valeur transmise) et vous pouvez avoir des paramètres de types compatibles. Vous obtiendriez une erreur si vous tentiez d'implémenter une méthode abstraite makeSound(input : number) : string {basée sur l'exemple ci-dessus, où inputdevrait être une chaîne. Type 'string' is not assignable to type 'number'..
Fenton
19

Si vous prenez la réponse d'Erics un peu plus loin, vous pouvez réellement créer une implémentation assez décente de classes abstraites, avec une prise en charge complète du polymorphisme et la possibilité d'appeler des méthodes implémentées à partir de la classe de base. Commençons par le code:

/**
 * The interface defines all abstract methods and extends the concrete base class
 */
interface IAnimal extends Animal {
    speak() : void;
}

/**
 * The abstract base class only defines concrete methods & properties.
 */
class Animal {

    private _impl : IAnimal;

    public name : string;

    /**
     * Here comes the clever part: by letting the constructor take an 
     * implementation of IAnimal as argument Animal cannot be instantiated
     * without a valid implementation of the abstract methods.
     */
    constructor(impl : IAnimal, name : string) {
        this.name = name;
        this._impl = impl;

        // The `impl` object can be used to delegate functionality to the
        // implementation class.
        console.log(this.name + " is born!");
        this._impl.speak();
    }
}

class Dog extends Animal implements IAnimal {
    constructor(name : string) {
        // The child class simply passes itself to Animal
        super(this, name);
    }

    public speak() {
        console.log("bark");
    }
}

var dog = new Dog("Bob");
dog.speak(); //logs "bark"
console.log(dog instanceof Dog); //true
console.log(dog instanceof Animal); //true
console.log(dog.name); //"Bob"

Étant donné que la Animalclasse nécessite une implémentation de, IAnimalil est impossible de construire un objet de type Animalsans avoir une implémentation valide des méthodes abstraites. Notez que pour que le polymorphisme fonctionne, vous devez contourner les instances de IAnimal, non Animal. Par exemple:

//This works
function letTheIAnimalSpeak(animal: IAnimal) {
    console.log(animal.name + " says:");
    animal.speak();
}
//This doesn't ("The property 'speak' does not exist on value of type 'Animal')
function letTheAnimalSpeak(animal: Animal) {
    console.log(animal.name + " says:");
    animal.speak();
}

La principale différence ici avec la réponse d'Erics est que la classe de base "abstraite" nécessite une implémentation de l'interface, et ne peut donc pas être instanciée seule.

Tiddo
la source
1
Pour moi au moins avec Typescript v1 - je ne peux pas référencer «ceci» depuis un constructeur pour passer en super. Pensées?
Kieran Benton
Quelle version exacte du compilateur utilisez-vous et quelle erreur obtenez-vous? tsc 1.0.1 compile parfaitement les extraits ci-dessus.
Tiddo
Le mot-clé 'this' n'est pas autorisé dans super (). Im using tsc 1.0.3
Zasz
C'est assez étrange. Utilisez-vous le compilateur CLI ou Visual Studio?
Tiddo
Moi aussi, je ne peux pas utiliser "this" dans l'appel super (). Je peux l'utiliser juste après pour définir le membre du parent pour l'implémentation enfant, mais cela n'impose pas l'extension de la classe abstraite. J'utilise le plugin Eclispe Typsscript de Palantir, v1.0.1. Je remarque que super (this) fonctionne très bien dans typescriptlang.org/Playground .
Eric
2

Je crois que l'utilisation d'une combinaison d'interfaces et de classes de base pourrait fonctionner pour vous. Il appliquera les exigences comportementales au moment de la compilation (rq_ post "ci-dessous" fait référence à un post ci-dessus, qui n'est pas celui-ci).

L'interface définit l'API comportementale qui n'est pas satisfaite par la classe de base. Vous ne pourrez pas définir des méthodes de classe de base pour appeler des méthodes définies dans l'interface (car vous ne pourrez pas implémenter cette interface dans la classe de base sans avoir à définir ces comportements). Peut-être que quelqu'un peut trouver un coffre - fort astuce pour autoriser l'appel des méthodes d'interface dans le parent.

Vous devez vous rappeler d'étendre et d'implémenter dans la classe que vous instancierez. Il répond aux préoccupations concernant la définition du code d'échec d'exécution. Vous ne pourrez également même pas appeler les méthodes qui gêneraient si vous n'avez pas implémenté l'interface (comme si vous essayez d'instancier la classe Animal). J'ai essayé d'avoir l'interface étendre le BaseAnimal ci-dessous, mais il a caché le constructeur et le champ 'nom' de BaseAnimal de Snake. Si j'avais pu le faire, l'utilisation d'un module et des exportations aurait pu empêcher une instanciation directe accidentelle de la classe BaseAnimal.

Collez-le ici pour voir si cela fonctionne pour vous: http://www.typescriptlang.org/Playground/

// The behavioral interface also needs to extend base for substitutability
interface AbstractAnimal extends BaseAnimal {
    // encapsulates animal behaviors that must be implemented
    makeSound(input : string): string;
}

class BaseAnimal {
    constructor(public name) { }

    move(meters) {
        alert(this.name + " moved " + meters + "m.");
    }
}

// If concrete class doesn't extend both, it cannot use super methods.
class Snake extends BaseAnimal implements AbstractAnimal {
    constructor(name) { super(name); }
    makeSound(input : string): string {
        var utterance = "sssss"+input;
        alert(utterance);
        return utterance;
    }
    move() {
        alert("Slithering...");
        super.move(5);
    }
}

var longMover = new Snake("windy man");

longMover.makeSound("...am I nothing?");
longMover.move();

var fulture = new BaseAnimal("bob fossil");
// compile error on makeSound() because it is not defined.
// fulture.makeSound("you know, like a...")
fulture.move(1);

Je suis tombé sur la réponse de FristvanCampen comme indiqué ci-dessous. Il dit que les classes abstraites sont un anti-modèle, et suggère que l'on instancie les classes «abstraites» de base en utilisant une instance injectée d'une classe d'implémentation. C'est juste, mais des arguments contraires sont avancés. Lisez par vous-même: https://typescript.codeplex.com/discussions/449920

Partie 2: J'ai eu un autre cas où je voulais une classe abstraite, mais on m'a empêché d'utiliser ma solution ci-dessus, car les méthodes définies dans la "classe abstraite" devaient se référer aux méthodes définies dans l'interface correspondante. Donc, j'utilise les conseils de FristvanCampen, en quelque sorte. J'ai la classe "abstraite" incomplète, avec les implémentations de méthodes. J'ai l'interface avec les méthodes non implémentées; cette interface étend la classe "abstraite". J'ai ensuite une classe qui étend la première et implémente la seconde (elle doit s'étendre à la fois car le super constructeur est inaccessible autrement). Voir l'exemple (non exécutable) ci-dessous:

export class OntologyConceptFilter extends FilterWidget.FilterWidget<ConceptGraph.Node, ConceptGraph.Link> implements FilterWidget.IFilterWidget<ConceptGraph.Node, ConceptGraph.Link> {

    subMenuTitle = "Ontologies Rendered"; // overload or overshadow?

    constructor(
        public conceptGraph: ConceptGraph.ConceptGraph,
        graphView: PathToRoot.ConceptPathsToRoot,
        implementation: FilterWidget.IFilterWidget<ConceptGraph.Node, ConceptGraph.Link>
        ){
        super(graphView);
        this.implementation = this;
    }
}

et

export class FilterWidget<N extends GraphView.BaseNode, L extends GraphView.BaseLink<GraphView.BaseNode>> {

    public implementation: IFilterWidget<N, L>

    filterContainer: JQuery;

    public subMenuTitle : string; // Given value in children

    constructor(
        public graphView: GraphView.GraphView<N, L>
        ){

    }

    doStuff(node: N){
        this.implementation.generateStuff(thing);
    }

}

export interface IFilterWidget<N extends GraphView.BaseNode, L extends GraphView.BaseLink<GraphView.BaseNode>> extends FilterWidget<N, L> {

    generateStuff(node: N): string;

}
Eric
la source
1

J'utilise pour lancer une exception dans la classe de base.

protected abstractMethod() {
    throw new Error("abstractMethod not implemented");
}

Ensuite, vous devez implémenter dans la sous-classe. Le contre est qu'il n'y a pas d'erreur de build mais d'exécution. Les avantages sont que vous pouvez appeler cette méthode à partir de la super classe, en supposant qu'elle fonctionnera :)

HTH!

Milton

Milton
la source
-20

Non non Non!N'essayez pas de créer vos propres classes et méthodes «abstraites» lorsque le langage ne prend pas en charge cette fonctionnalité; il en va de même pour toute fonctionnalité de langue que vous souhaitez qu'une langue donnée soit prise en charge. Il n'existe aucun moyen correct d'implémenter des méthodes abstraites dans TypeScript. Structurez simplement votre code avec des conventions de dénomination telles que certaines classes ne soient jamais directement instanciées, mais sans appliquer explicitement cette interdiction.

En outre, l'exemple ci-dessus ne fournira cette application qu'au moment de l'exécution, PAS au moment de la compilation, comme vous pouvez vous y attendre en Java / C #.

rq_
la source
4
Je peux voir d'où vous venez, mais je ne suis respectueusement pas d'accord. Si un langage implémente quelque chose, il est mauvais de le réimplémenter vous-même. Mais si vous n'avez pas quelque chose, vous n'avez pas d'autre choix que de le mettre en œuvre vous-même. Bien sûr, vous ne verrez pas les problèmes avant l'exécution, mais lever une exception la première fois que vous testerez quelque chose vous permettra de savoir que vous avez raté assez rapidement. Ce n'est pas l'idéal bien sûr - c'est pourquoi, OMI, Typescript a besoin d'un support de classe abstraite. Jusqu'à ce qu'il fasse cependant ...
Maverick
Je souhaitais que JavaScript ait des classes, l'inférence de type, le typage statique et les interfaces, et devinez quoi, Typescript l'a. Ce serait la même chose pour la méthode abstraite, le compilateur doit juste vérifier que toute classe étendant la classe abstraite implémente la méthode abstraite, comme elle le fait déjà pour les interfaces (une interface est essentiellement juste une classe avec uniquement une méthode abstraite)
Tony BenBrahim
1
J'ai tendance à être d'accord avec @rq_ ici. Le point des méthodes abstraites est d'obtenir une validation au moment de la compilation que le programme ne peut pas entrer dans un état invalide. Les solutions proposées vous donnent juste des vérifications d'exécution, ce qui signifie que lorsque votre programme s'exécute, vous ne pouvez pas être sûr qu'il est dans un état valide. Cela signifie que vous devez opérer sous l'hypothèse que la méthode n'est pas implémentée et faire preuve de prudence en conséquence. Mentir que vous avez des méthodes abstraites, c'est simplement demander d'être mordu par un comportement d'exécution inattendu.
Micah Zoltu