TypeScript:使用类作为接口

23

我试图将一个类用作另一个类的接口。通过这种方式,我正在尝试为测试实现一个最新的mockClass。

这应该是可能的,也是一种有利的方法,如https://angular.io/guide/styleguide#interfaces所述。

并且在此处演示:Export class as interface in Angular2

但奇怪的是,在使用Typescript 2.5.3和VSCode 1.17.2时出现了错误。

SpecialTest类中的错误。

[ts]
Class 'SpecialTest' incorrectly implements interface 'Test'.
  Types have separate declarations of a private property 'name'.

示例代码:

class Test {
    private name: string;

    constructor(name: string) {
        this.name = name;
    }

    getName() {
        return this.name;
    }
    setName(name: string): void {
        this.name = name;
    }
}

class SpecialTest implements Test {
    private name: string;

    getName(): string {
        return '';
    }
    setName(name: string): void {
    }
}

我错过了什么?

编辑: 按照 @fenton 的建议,使用 string 替代 String


1
阅读错误信息,如果 SpecialTest 实现了 Test,它需要声明私有属性 name 吗? - stealththeninja
是的。如果它没有实现接口,那么它会失败。我们使用类作为接口…… - jvoigt
5个回答

30

在开始之前,当您扩展一个类时,您需要使用extends关键字。您可以扩展一个类并实现一个接口。此内容更多详细说明请参见本文后面的附注。

class SpecialTest extends Test {

此外,要注意区分stringString,因为这可能会让你犯错。你的类型注释几乎肯定应该是string(小写)。

最后,你不需要手动分配构造函数参数,因此原始代码:

class Test {
    private name: string;

    constructor(name: string) {
        this.name = name;
    }

    // ...
}

更好地表达为:

class Test {
    constructor(private name: string) {
    }

    // ...
}

现在您可以从多种解决方案中选择适合您的问题。

受保护成员

name 成员设置为受保护,然后您就可以在子类中使用它:

class Test {
    protected name: string;

    constructor(name: string) {
        this.name = name;
    }

    getName() {
        return this.name;
    }
    setName(name: string): void {
        this.name = name;
    }
}

class SpecialTest extends Test {
    getName(): string {
        return '';
    }
    setName(name: string): void {
    }
}

接口

这是我认为最符合您需求的解决方案。

如果将公共成员提取到接口中,您应该能够将这两个类视为该接口(无论是否显式使用 implements 关键字 - TypeScript 是结构类型)。

interface SomeTest {
  getName(): string;
  setName(name: string): void;
}

如果您希望的话,可以明确地实现它:

class SpecialTest implements SomeTest {
    private name: string;

    getName(): string {
        return '';
    }
    setName(name: string): void {
    }
}

现在您的代码可以依赖于接口而不是具体类。

将类用作接口

从技术上讲,将一个类作为接口进行引用是可行的,但如果使用implements MyClass则会遇到问题。

首先,对于任何以后必须阅读您代码的人(包括未来的您),您都正在添加不必要的复杂性。您还在使用一种模式,这意味着您需要小心关键字。在继承的类被更改时,意外使用extends可能会导致棘手的错误。维护人员将需要成为有眼力的鹰眼,了解使用了哪个关键字。而所有这些只是为了保留结构语言中的名义惯例。

接口是抽象的,不太可能改变。类更加具体,更容易改变。将类用作接口破坏了依赖于稳定抽象概念的整个概念...而转而使您依赖于不稳定的具体类。

考虑在程序中出现的“将类用作接口”的泛滥。类的更改(比如我们添加一个方法)可能会无意中导致更改波及广泛...由于输入不包含甚至没有使用的方法,因此程序的多少部分现在会拒绝输入?

更好的替代方案(当不存在访问修饰符兼容性问题时)是...

从类中创建一个接口:

interface MyInterface extends MyClass {
}

或者,在第二个类中根本不引用原始类。允许结构类型系统检查兼容性。

顺便提一下......根据您的TSLint配置,弱类型(例如仅包含可选类型的接口)将触发no-empty-interface规则。

私有成员的特例

这些方法(使用类作为接口、从类生成接口或结构类型)都无法解决私有成员的问题。这就是为什么解决真正问题的解决方案是创建一个带有公共成员的接口。

在私有成员的特定情况下,比如在问题中,让我们想想如果继续使用原始模式会发生什么?自然的结果将是为了保持使用类作为接口的模式,成员的可见性将被改变,就像这样:

class Test {
    public name: string;

    constructor(name: string) {
        this.name = name;
    }

    getName() {
        return this.name;
    }
    setName(name: string): void {
        this.name = name;
    }
}

现在我们正在打破更加成熟的面向对象编程原则。


1
TypeScript和Angular在其指南方面有点奇怪。无论如何,OP想要通过使用普通类作为接口来遵循Angular/TypeScript的指南,而不是TS提供的实际接口结构(因此他也不想扩展某些东西)。 - malifa
1
感谢@Fenton! 采用这种方式的原因是: 我想定期使用测试类,但实现一个模拟测试。因此,当更改原始类时,整个模拟应该抛出错误,因为它需要反映更改。 - jvoigt
我测试了你的所有建议!它们都运行良好 :)注意:接口声明不能是空的 [tslint] no-empty-interface。因此,可以添加一些琐碎的东西。 - jvoigt
1
@jvoigt - 这是您在 TSLint 配置中做出的有意识的选择 - TypeScript 允许这些,它们被称为弱类型。它们在像 jQuery 这样的库中很常见,其中选项对象与默认值合并,允许您指定任何喜欢的成员或不指定。 - Fenton

23
要从一个类创建一个接口,只捕获公共属性而不是具体的实现细节,你可以使用 Pick<Type, Keys> 结合 keyof 运算符。
class Test {
  foo: any;
  private bar: any;
}

class SepcialTest implements Pick<Test, keyof Test> {
  foo: any;
}
Pick<Type, Keys> 构造一个类型,从Type中挑选出一组属性Keys。
[...] keyof T,索引类型查询操作符。对于任何类型T,keyof T是T的已知公共属性名称的联合。
在给定的示例中,Pick<Test, keyof Test>解析为Pick<Test, 'foo'>,即{ foo: any }

哇。这可能会派上用场。 - jvoigt
1
这很完美。我有一个Api类,它正在使用MockApi进行模拟。我想避免额外的基础接口,因为它纯粹是为了测试。干得好!这应该被接受的答案。 - phatmann

3
这个例子中,父类应该是抽象的并且充当接口。TypeScript接口没有访问修饰符,所有接口属性都应该是公共的。
当像Test这样的具体类被用作接口时,会出现问题,因为实现无法实现私有属性,也无法覆盖它们。 TestSpecialTest都应该实现一些抽象类作为接口(在这种情况下,抽象类只能有公共成员),或者从它继承(在这种情况下,抽象类可以有protected成员)。

2

在跟进@Fenton对于“使用类作为接口”的解释之后,有更明确的方式来声明class SpecialTest implements Test,即使用InstanceType<T>实用类型。例如:class SpecialTest implements InstanceType<typeof Test>。我发现在阅读代码时,这样做会更少令人困惑。

在您自己的代码中首选接口,只有在需要为第三方类创建装饰器时,我才发现这种“魔法”有用/必要。


1
上帝保佑你。这真是救了我的一天。 - undefined

0
在你链接的这篇文章中this,你在实现中完成了最后一部分吗?

It should be noticed that any class can be used as an interface in >TypeScript. So if there's no real need to differentiate between interface >and implementation, it may be just one concrete class:

export class Foo {
    bar() { ... }

    baz() { ... }
 }

...
provider: [Foo]
...

Which may be used later as an interface if necessary:

 export class SomeFoo implements Foo { ... }

 ...
 provider: [{ provide: Foo, useClass: SomeFoo }]
 ...

提供者是 Angular 特定的东西,不是吗?当类是可注入的时,您应该为组件和模块提供类,这样它就会被添加到注入器的提供程序池中。 - jvoigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接