正如 JB Nizet 非常正确地指出的那样,由 HTTP 请求产生的反序列化 JSON 值永远不会是类的实例。
虽然class
TypeScript 中构造的双重角色(见下文)使得使用 aclass
来描述这些响应值的形状成为可能,但这是一种糟糕的做法,因为响应文本将被反序列化为纯 JavaScript 对象。
JavaScript 和 TypeScript 中的类声明:
在 JavaScript 中,类声明
class Comment {
constructor(likes, comment) {
this.likes = likes;
this.comment = comment;
}
}
创建一个可以实例化的值,new
用作本质上是一个工厂。
在 TypeScript 中,类声明会创建两件事。
第一个是与上面描述的完全相同的 JavaScript 类值。第二种是描述通过写创建的实例的结构的类型
new Comment(4, 'I love your essays')
然后可以将第二个工件类型用作类型注释,例如在您的示例中
register(): Observable<Comment[]> {
return this.http.get()
}
这表示register
返回一个实例Observable
数组Comment
class
。
现在假设您的 HTTP 请求返回以下 JSON
[
{
"likes": 4,
"comment": "I love you oh so very much"
},
{
"likes": 1,
"comment": "I lust after that feeling of approval that only likes can bring"
}
]
但是方法声明
register(): Observable<Comment[]>;
虽然它正确地允许调用者写
register().subscribe(comments => {
for (const comment of comment) {
if (comment.likes > 0) {
likedComments.push(comment);
}
}
});
这一切都很好,不幸的是,它也允许调用者编写类似的代码
getComments() {
register().subscribe(comments => {
this.comments = comments;
});
}
getTopComment() {
// since there might not be any comments
// it is likely that a check will be made here
const [topComment] = this.comments.slice().sort((x, y) => y - x);
// BUG! Always false at runtime.
if (topComment instanceof Comment) {
return topComment;
}
}
由于注释实际上不是Comment
类的实例,因此上述检查将始终失败,因此代码中存在错误。但是,TypeScript 不会捕获错误,因为我们说它comments
是Comment
类的实例数组,这将使检查有效(回想一下,可以将response.json()
返回any
值转换为任何类型而不会发出警告,因此在编译时一切看起来都很好)。
但是,如果我们已将评论声明为 interface
interface Comment {
comment: string;
likes: number;
}
thengetComments
将继续进行类型检查,因为它实际上是正确的代码,但getTopComment
会在 if 语句的编译时引发错误,因为正如许多其他人所指出的, aninterface
是仅编译时的构造,不能用作 if它是一个执行instanceof
检查的构造函数。编译器会告诉我们有错误。
评论:
除了给出的所有其他原因之外,在我看来,当您在 JavaScript/TypeScript 中有代表普通旧数据的内容时,使用类通常是过度的。它创建了一个带有原型的函数,并具有许多我们可能不需要或不关心的其他方面。
如果您使用对象,它也会丢弃您默认获得的好处。这些好处包括用于创建和复制对象的语法糖以及 TypeScript 对这些对象类型的推断。
考虑
import Comment from 'app/comment';
export default class CommentService {
async getComments(): Promse<Array<Comment>> {
const response = await fetch('api/comments', {httpMethod: 'GET'});
const comments = await response.json();
return comments as Comment[]; // just being explicit.
}
async createComment(comment: Comment): Promise<Comment> {
const response = await fetch('api/comments', {
httpMethod: 'POST',
body: JSON.stringify(comment)
});
const result = await response.json();
return result as Comment; // just being explicit.
}
}
如果Comment
是一个接口,我想使用上面的服务来创建评论,我可以这样做
import CommentService from 'app/comment-service';
export async function createComment(likes: number, comment: string) {
const commentService = new CommentService();
await commentService.createCommnet({comment, likes});
}
如果Comment
是一类,我需要通过迫使该引进一些锅炉板import
的Comment
。自然,这也会增加耦合。
import CommentService from 'app/comment-service';
import Comment from 'app/comment';
export async function createComment(likes, comment: string) {
const commentService = new CommentService();
const comment = new Comment(likes, comment); // better get the order right
await commentService.createCommnet(comment);
}
这是两行额外的行,其中一个涉及依赖另一个module来创建对象。
现在 ifComment
是一个接口,但我想要一个复杂的类,在我将它提供给我的服务之前进行验证等等,我仍然可以拥有它。
import CommentService from 'app/comment-service';
import Comment from 'app/comment';
// implements is optional and typescript will verify that this class implements Comment
// by looking at the definition of the service method so I could remove it and
// also remove the import statement if I wish
class ValidatedComment implements Comment {
constructor(public likes, public comment: string) {
if (likes < 0 || !Number.isSafeInteger(likes)) {
throw RangeError('Likes must be a valid number >= 0'
}
}
}
export async function createComment(likes, comment: string) {
const commentService = new CommentService();
const comment = new ValidatedComment(likes, comment); // better get the order right
await commentService.createCommnet(comment);
}
简而言之,interface
在使用 TypeScript 时,使用 an来描述响应的类型以及与 HTTP 服务交互的请求有很多原因。
注意:您也可以使用type
声明,它同样安全和健壮,但不太惯用,并且周围的工具interface
通常使其更适合这种情况。