Declarando o método abstrato no TypeScript


195

Estou tentando descobrir como definir corretamente métodos abstratos no TypeScript:

Usando o exemplo de herança original:

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);
    }
}

Gostaria de saber como definir corretamente o método makeSound, para que seja digitado e possível sobrescrever.

Além disso, não sei como definir protectedmétodos corretamente - parece ser uma palavra-chave, mas não tem efeito e o código não será compilado.


4
Classes e métodos abstratos agora são um novo recurso do próximo TypeScript 1.6.
falconepl

Respostas:


284

A namepropriedade está marcada comoprotected . Isso foi adicionado no TypeScript 1.3 e agora está firmemente estabelecido.

O makeSoundmétodo está marcado como abstract, assim como a classe. Você não pode instanciar diretamente um Animalagora, porque é abstrato. Isso faz parte do TypeScript 1.6 , que agora está oficialmente ativo .

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);
    }
}

A maneira antiga de imitar um método abstrato era gerar um erro se alguém o usasse. Você não precisa mais fazer isso quando o TypeScript 1.6 chegar ao seu projeto:

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);
    }
}

É um comportamento normal, que o compilador não reclame se eu perder um parâmetro, alterar um tipo de parâmetro ou alterar o tipo de retorno ao substituir um método abstrato?
Vetterjack 5/12

1
É válido omitir um parâmetro (se você não o usar, poderá ignorar qualquer valor que seja passado) e poderá ter parâmetros de tipos compatíveis. Você obteria um erro se tentasse implementar um método abstrato, com makeSound(input : number) : string {base no exemplo acima, onde inputdeveria haver uma string. Type 'string' is not assignable to type 'number'..
Fenton

19

Se você levar a resposta do Erics um pouco mais longe, poderá criar uma implementação bastante decente de classes abstratas, com suporte total ao polimorfismo e a capacidade de chamar métodos implementados da classe base. Vamos começar com o código:

/**
 * 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"

Como a Animalclasse requer uma implementação IAnimal, é impossível construir um objeto do tipo Animalsem ter uma implementação válida dos métodos abstratos. Observe que, para que o polimorfismo funcione, você precisa repassar instâncias de IAnimal, não Animal. Por exemplo:

//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();
}

A principal diferença aqui com a resposta do Erics é que a classe base "abstrata" requer uma implementação da interface e, portanto, não pode ser instanciada por si mesma.


1
Para mim, pelo menos com o Typescript v1 - não posso fazer referência a 'this' de dentro de um construtor para passar para super. Pensamentos?
Kieran Benton

Qual versão exata do compilador você usa e que erro você recebe? O tsc 1.0.1 compila perfeitamente os trechos acima.
Tiddo

A palavra-chave 'this' não é permitida em super (). Estou usando o tsc 1.0.3
Zasz

Isso é bem estranho. Você usa o compilador CLI ou o Visual Studio?
Tiddo

Eu também não posso usar "this" na chamada super (). Eu posso usá-lo logo depois, para definir o membro do pai para a implementação filho, mas isso não impõe a extensão da classe abstrata. Estou usando o plug-in Eclispe Typsscript da Palantir, v1.0.1. Percebo que super (isso) funciona bem em typescriptlang.org/Playground .
Eric

2

Acredito que o uso de uma combinação de interfaces e classes base possa funcionar para você. Ele aplicará requisitos comportamentais em tempo de compilação (rq_ post "below" refere-se a um post acima, que não é este).

A interface define a API comportamental que não é atendida pela classe base. Você não poderá definir métodos da classe base para chamar os métodos definidos na interface (porque não poderá implementar essa interface na classe base sem precisar definir esses comportamentos). Talvez alguém possa inventar um truque seguro para permitir a chamada dos métodos de interface no pai.

Você deve se lembrar de estender e implementar na classe que você instanciará. Satisfaz as preocupações sobre a definição do código de falha de tempo de execução. Você também não poderá chamar os métodos que vomitariam se não implementasse a interface (como se você tentasse instanciar a classe Animal). Tentei fazer com que a interface estendesse o BaseAnimal abaixo, mas ocultou o construtor e o campo 'name' do BaseAnimal do Snake. Se eu pudesse fazer isso, o uso de um módulo e as exportações poderiam ter impedido a instanciação direta acidental da classe BaseAnimal.

Cole isso aqui para ver se funciona para você: 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);

Me deparei com a resposta de FristvanCampen como link abaixo. Ele diz que as classes abstratas são um antipadrão e sugere que uma instância instale classes 'abstratas' usando uma instância injetada de uma classe de implementação. Isso é justo, mas existem contra-argumentos. Leia você mesmo: https://typescript.codeplex.com/discussions/449920

Parte 2: tive outro caso em que desejava uma classe abstrata, mas fui impedido de usar minha solução acima, porque os métodos definidos na "classe abstrata" precisavam se referir aos métodos definidos na interface correspondente. Então, eu uso o conselho de FristvanCampen, mais ou menos. Eu tenho a classe "abstrata" incompleta, com implementações de métodos. Eu tenho a interface com os métodos não implementados; essa interface estende a classe "abstrata". Em seguida, tenho uma classe que estende o primeiro e implementa o segundo (deve estender ambos porque o super construtor é inacessível de outra forma). Veja o exemplo (não executável) abaixo:

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;
    }
}

e

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;

}

1

Eu uso para lançar uma exceção na classe base.

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

Então você tem que implementar na subclasse. Os contras é que não há erro de compilação, mas tempo de execução. A vantagem é que você pode chamar esse método da super classe, supondo que ele funcione :)

HTH!

Milton


-20

Não não não! Por favor, não tente criar suas próprias classes e métodos 'abstratos' quando o idioma não suportar esse recurso; o mesmo vale para qualquer recurso de idioma que você deseja que um determinado idioma seja suportado. Não há maneira correta de implementar métodos abstratos no TypeScript. Apenas estruture seu código com convenções de nomenclatura, de modo que determinadas classes nunca sejam instanciadas diretamente, mas sem impor explicitamente essa proibição.

Além disso, o exemplo acima apenas fornecerá essa imposição em tempo de execução, NÃO em tempo de compilação, como seria de esperar em Java / C #.


4
Posso ver de onde você vem, mas discordo respeitosamente. Se uma linguagem implementa algo, é ruim reimplementá-lo você mesmo. Mas se você não tem algo, não tem escolha a não ser implementá-lo de alguma forma. Claro, você não verá os problemas até o tempo de execução, mas lançar uma exceção na primeira vez em que testar algo permitirá que você saiba que é burro rapidamente. Naturalmente, não é o ideal - e é por isso que o IMO Typescript precisa de suporte de classe abstrata. Até que isso aconteça no entanto ...
Maverick

Eu desejava que o JavaScript tivesse classes, inferência de tipo, tipagem estática e interfaces e, adivinhe, o Typescript possui. Seria o mesmo para o método abstrato, o compilador apenas precisa verificar se qualquer classe que estende a classe abstrata implementa o método abstrato, como já faz para interfaces (uma interface é essencialmente apenas uma classe com apenas o método abstrato)
Tony BenBrahim

1
Costumo concordar com @rq_ aqui. O objetivo dos métodos abstratos é obter a validação do tempo de compilação que o programa não pode entrar em um estado inválido. As soluções propostas apenas fornecem verificações de tempo de execução, o que significa que, quando o programa é executado, você não pode ter certeza de que está em um estado válido. Isso significa que você deve estar operando com o pressuposto de que o método não foi implementado e resguardar adequadamente. Mentir para si mesmo que você tem métodos abstratos é apenas pedir para ser mordido por um comportamento inesperado do tempo de execução.
Micah Zoltu
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.