TypeScript中的接口和类

30

在C#中,接口和类之间有很大的区别。事实上,类表示引用类型,因此我们可以创建基于该类的对象,而接口旨在是一种合同,类签署该合同以确保某种行为的存在。特别地,我们不能创建接口的实例。

使用接口的整个重点在于公开行为。通过提供所述行为的一个显式实现,类来实现它。

在这种情况下,尽管接口可能包含属性,但大多数情况下,我们关心接口是因为行为问题。所以,在大多数情况下,接口只是行为的合同。

然而,在TypeScript中,我看到了一些让我感到不安的内容,实际上我不止一次看到过这样的情况,这也是我提出这个问题的原因。

在一个教程中,我看到了以下内容:

export interface User {
    name: string; // required with minimum 5 chracters
    address?: {
        street?: string; // required
        postcode?: string;
    }
}

但是等一下。为什么User是一个接口?如果我们像C#那样思考,User不应该是一个接口。实际上,看起来我们正在定义数据类型User,而不是行为契约。

像我们在C#中所做的那样思考,自然的事情应该是这样的:

export class User {
    public name: string;
    public address: Address;
}
export class Address {
    public street: string;
    public postcode: string;
}

但是在TypeScript中,像我们在类中使用接口定义数据类型而不是行为契约的方式似乎非常普遍。

那么,在TypeScript中接口的作用是什么?为什么人们像在C#中使用类一样在TypeScript中使用接口?接口应该如何正确地使用:是用于建立行为契约,还是定义属性和对象应具有的属性?


4
由于TypeScript是结构类型的,所以从C#中可以应用的内容非常少。此外,拥有某些属性绝对可以被认为是一种行为 :-) - artem
我不知道你期望什么样的答案。你不能这样比较编程语言。 - nozzleman
4
我认为引用C#是一个错误。我不想比较编程语言或者类似的内容。我只是以C#为例子,说明我熟悉接口的使用方式。我想知道的是,在TypeScript中应该何时使用接口或类,因为人们似乎都将它们用于相同的目的,而不是为它们分别设定特定的用途。就像我之前说过的,使用类和接口可以实现同样的功能,我也看到其他人都在这么做,这让我感到困惑。我的问题只与TypeScript有关,C#只是一个例子。 - user1620696
1
好的,但是在TypeScript中,接口被用于类似的目的,但你不能将它们互换使用。例如,你不能在给定一个接口IUser的情况下执行new IUser(),所以从我的角度来看,如果你能更具体地说明什么让你感到困惑,那就太好了。是“嵌套内联”接口还是可选属性? - nozzleman
5个回答

27

请考虑在JavaScript中,数据通常作为普通对象交换,通常通过JSON传输:

let data = JSON.parse(someString);

假设这个data是一个包含User对象的数组,并且我们将把它传递给一个函数:

假设这个data是一个包含User对象的数组,并且我们将把它传递给一个函数:

data.forEach(user => foo(user))

foo 就应该像这样输入:

function foo(user: User) { ... }

等等,我们从未执行过new User!我们应该吗?即使结果是相同的,一个具有属性的Object,我们是否必须编写一个User类并将所有data映射到它上面?不,那样就为了满足类型系统而疯狂了,但对运行时没有任何改变。这里完全足够简单的interface来描述特定对象的预期外观("行为")。


2
是的,data.forEach(foo)... 我只是为了清晰而使用了长手写法。 :o) - deceze
仅为满足类型系统而做出这种操作是疯狂的,但对运行时不会有任何变化。- 这一句话让我茅塞顿开,谢谢 :) 那么...如果我说使用太多类型系统会失去底层 JavaScript 的动态特性,我的说法正确吗? - asolvent
2
@ASolvent 那种动态性是一把双刃剑...使用简单接口的鸭子类型对象很棒,但上下文中对简单类型进行隐式转换可能会成为陷阱。你需要找到一个平衡点:必要时固定类型,足够时使用鸭子类型。 - deceze
1
这种方法只有在您对数据源有100%的信任或者畸形数据不会在后续引起问题时才足够。 - Automatico

17

我也是从C#背景转到Typescript的,曾经有同样的疑问。我想到了POCOs(POTO是一种东西吗?)

那么在Typescript中接口的作用是什么?

Typescript手册似乎表明接口是用于"在你的代码中定义合同"。

为什么人们在Typescript中使用接口就像我们在C#中使用类一样?

我同意@deceze在这里的回答。

John Papa在他的博客中扩展了关于类和接口的主题。他建议类最适合于"创建多个新实例,使用继承,[和]单例对象"。因此,基于Typescript手册所描述的Typescript接口的意图和一个人的观点,似乎在Typescript中不需要使用类来建立合同。相反,应该使用接口。(你的C#感官仍然会受到冒犯。)

如果我理解正确,您正在问接口应该建立行为契约还是结构契约。对此,我的回答是:两者都可以。TypeScript接口仍然可以像C#或Java中使用接口一样(即描述类的行为),但它们还提供了描述数据结构的能力。
此外,我的同事让我使用接口而不是类,因为接口在编译器中不会产生代码。
示例:
这个TypeScript:
class Car implements ICar {
    foo: string;
    bar(): void {
    }
}

interface ICar {
    foo: string;
    bar(): void;
}

生成以下Javascript代码:

var Car = (function () {
    function Car() {
    }
    Car.prototype.bar = function () {
    };
    return Car;
}());

试一试


11

在TypeScript中,接口与C#中的接口类似,它们都提供了一个契约。然而,与仅包含方法的C#接口相反,TypeScript接口也可以描述对象包含的字段或属性。因此,它们也可用于一些使用C#接口无法直接完成的事情。

TypeScript中接口与类之间的一个主要区别是,接口没有运行时表示,并且不会为其生成任何代码。接口非常广泛地可用。例如,您可以使用对象字面量构造满足接口的对象。例如:

let user: User = {
  name: 'abc',
  address: {
    street: 'xyz',
  },
};

或者你可以将任何数据对象(例如通过JSON解析接收)分配给一个接口(但是您的预检查应该断言它确实是有效数据)。因此,接口对于数据非常灵活。

另一方面,类在运行时与其关联一个类型,并且有代码生成。您可以使用 instanceof 在运行时检查类型,并设置原型链。如果将User定义为类,则除非调用构造函数,否则它不会是有效用户。您不能只定义任何适当类型的数据为User。您需要创建一个新实例并复制属性。

我的个人经验法则:

  • 如果我处理纯数据(来自不同来源),我使用接口
  • 如果我建模的东西具有身份和状态(并且可能附带方法以修改状态),我使用类。

好的回答,尤其是你的经验法则。我发现它们非常有帮助。谢谢! - ntaso
不错的回答 - 除了最后一句话,你说“还有可能是方法”-但如果你使用这样一个类通过JSON.stringify()发送数据到API,你最终会得到一个空对象{},因为它不知道如何处理那些方法。请参阅@JasonKleban的下面的答案。 - rmcsharry

2
在TypeScript中,接口是形状契约,描述对象的预期结构。如果一个值有某个接口注释,你期望它是一个具有接口定义成员的对象。成员可以是值或函数(方法)。通常,它们的行为(函数体)不是契约的一部分。但是您可以指定它们是否为readonly
那么在TypeScript中接口的作用是什么呢?为什么人们会像在C#中使用类一样使用TypeScript中的接口?
如果预计TypeScript类实现接口,则Typescript接口可以扮演与C#接口相同的角色。
但不仅类可以实现接口;任何类型的值都可以。
interface HelloPrinter {
    printHello(): void
}

以下对象不是一个类,但仍然实现了该接口:
{
    printHello: () => console.log("hello")
}

因此,我们可以这样做。
const o: HelloPrinter = {
    printHello: () => console.log("hello")
}

同时TypeScript编译器不会报错。

该对象实现了我们的接口,而无需强制我们编写一个类。

使用接口比使用(接口和)类更轻量级。

但是,如果您需要在运行时知道类型名称(类/接口名称),则类是正确的选择,因为接口名称仅在编译时已知。


1
仅使用本地反序列化机制,您无法将特定类的实例反序列化。您只能反序列化为普通的JavaScript对象。这些对象可以遵循TypeScript接口,但不能是类的实例。如果您需要处理跨越序列化边界的数据(例如从Web服务预期的数据),请使用接口。如果您需要自己生成此类值的新实例,请直接构造它们或创建一个返回符合该接口的对象的便捷函数。
类本身可以实现接口,但如果您希望处理本地构造的类实例和反序列化、重建的普通对象,则可能会变得混乱。您永远无法依赖于对象的类基础,因此也没有将其定义为类以实现该目的的好处。
我成功创建了一个ServerProxy模块,负责发送代码并从Web服务中传回结果。如果您绑定到knockout模型或类似模型,则可以拥有一个类,该类通过构造函数将符合Web服务接口的普通JavaScript对象提升为您的模型类的实例。

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