在TypeScript中声明抽象方法


195

我试图弄清楚如何在TypeScript中正确定义抽象方法:

使用原始继承示例:

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

我想知道如何正确定义方法makeSound,因此可以键入它并且可以改写。

另外,我不确定如何正确定义protected方法-它似乎是一个关键字,但是没有效果,并且代码无法编译。


4
现在,抽象类和方法是即将推出的TypeScript 1.6的新功能。
falconepl

Answers:


284

name属性被标记为protected。它是在TypeScript 1.3中添加的,现已牢固建立。

makeSound方法abstract与类一样被标记为。您不能直接实例化Animalnow,因为它是抽象的。这是TypeScript 1.6的一部分,现已正式启用。

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

模仿抽象方法的旧方法是,如果有人使用过,则会引发错误。一旦TypeScript 1.6进入您的项目,您就无需再执行以下操作:

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

覆盖抽象方法时,如果我错过了一个参数,更改了参数类型或更改了返回类型,编译器不会抱怨是正常的行为吗?
Vetterjack's

1
省略参数是有效的(如果不使用它,则可以忽略传递的任何值),并且可以具有兼容类型的参数。如果尝试makeSound(input : number) : string {根据上述示例(其中input应为字符串)来实现抽象方法,则会出现错误。Type 'string' is not assignable to type 'number'.
Fenton

19

如果您进一步提出Erics的答案,您实际上可以创建一个相当不错的抽象类实现,同时全面支持多态性并能够从基类调用实现的方法。让我们从代码开始:

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

由于Animal该类需要实现,IAnimal因此Animal没有有效的抽象方法实现就无法构造类型的对象。请注意,要使多态性起作用,您需要传递而IAnimal不是的实例Animal。例如:

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

这里与Erics答案的主要区别在于“抽象”基类需要接口的实现,因此无法单独实例化。


1
至少对我来说,使用Typescript v1-我无法从构造函数中引用“ this”传递给super。有什么想法吗?
基兰·本顿

您使用哪个编译器的确切版本,并且遇到什么错误?tsc 1.0.1可以完美地编译上述片段。
Tiddo

super()中不允许使用'this'关键字。我正在使用tsc 1.0.3
Zasz 2014年

真奇怪。您使用CLI编译器还是Visual Studio?
Tiddo 2014年

我也不能在super()调用中使用“ this”。我可以在之后立即使用它来设置子实现的父级成员,但这不会强制抽象类的扩展。我正在使用Palantir的Eclispe Typsscript插件v1.0.1。我注意到super(this)在typescriptlang.org/Playground中可以正常工作。
埃里克

2

我相信结合使用接口和基类可以为您工作。它将在编译时强制执行行为要求(rq_帖子“以下”是指上面的帖子,而不是此帖子)。

该接口设置了基类无法满足的行为API。您将无法设置基类方法来调用接口中定义的方法(因为如果不定义这些行为,就无法在基类中实现该接口)。也许有人可以提出一个安全的技巧,以允许在父级中调用接口方法。

您必须记住要在要实例化的类中进行扩展和实现。它满足了有关定义运行时失败代码的担忧。如果您尚未实现接口(例如,如果您尝试实例化Animal类),您甚至将无法调用会呕吐的方法。我试图让接口扩展下面的BaseAnimal,但是它从Snake隐藏了构造函数和BaseAnimal的“名称”字段。如果我能够做到这一点,则使用模块和导出可以避免意外地直接实例化BaseAnimal类。

将此粘贴在这里,看看是否适合您: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);

我遇到了FristvanCampen的回答,如下所示。他说抽象类是一种反模式,并建议使用实例化类的注入实例实例化基础“抽象”类。这是公平的,但有相反的论点。亲自阅读:https//typescript.codeplex.com/discussions/449920

第2部分:在另一种情况下,我想要一个抽象类,但是由于“抽象类”中定义的方法需要引用匹配接口中定义的方法,因此无法使用我的解决方案。因此,我采用了FristvanCampen的建议。我有不完整的“抽象”类,带有方法实现。我有未实现方法的接口;此接口扩展了“抽象”类。然后,我有一个扩展第一个类并实现第二个类的类(它必须同时扩展两个类,因为否则无法访问超级构造函数)。请参见下面的(不可运行的)示例:

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

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

我经常在基类中引发异常。

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

然后,您必须在子类中实现。缺点是没有构建错误,但是有运行时错误。优点是,您可以从超类调用此方法,前提是它会起作用:)

HTH!

米尔顿


-20

不不不!如果该语言不支持该功能,请不要尝试制作自己的“抽象”类和方法。您希望支持给定语言的任何语言功能也是如此。没有正确的方法来在TypeScript中实现抽象方法。只需使用命名约定来构造代码,这样就永远不会直接实例化某些类,但不会显式地强制执行此禁止。

另外,上面的示例仅在运行时提供了这种实施,而在编译时则没有,就像您在Java / C#中所期望的那样。


4
我可以看到您来自哪里,但我不同意。如果一种语言实现了某种东西,那么自己重新实现它是很不好的。但是,如果您没有任何东西,那么您别无选择,只能以某种方式自己实现它。当然,直到运行时您都不会看到问题,但是在第一次测试时抛出异常将使您很快就感到无聊。当然这不是理想的-这就是IMO Typescript需要抽象类支持的原因。但是,直到做到...
Maverick

我希望JavaScript具有类,类型推断,静态类型和接口,并猜想Typescript有它。抽象方法将是相同的,编译器只需检查扩展抽象类的任何类都实现了抽象方法,就像它已经为接口所做的一样(接口本质上只是具有抽象方法的类)
Tony BenBrahim

1
我倾向于在这里同意@rq_。抽象方法的要点是获得编译时验证,以使程序无法进入无效状态。提出的解决方案只是为您提供运行时检查,这意味着您的程序在运行时无法确定其处于有效状态。这意味着您应该在未实施该方法的假设下进行操作,并进行相应的保护。对自己撒谎,您拥有抽象方法只是要求被意外的运行时行为所咬。
米卡·佐尔图
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.