Búsqueda de sitios web

javascript - ¿Cuál es la diferencia entre usar una clase y una interfaz?


¿Cuál es la diferencia entre hacer esto?

export class Comment {
  likes: string;
  comment: string;

  constructor(likes: string, comment: string){
    this.comment = comment;
    this.likes = likes;
  }
}

y esto

export interface CommentInterface {
  likes: string;
  comment: string;
}

en relación con declarar un tipo observable

register: Observable<CommentInterface[]> {
    return this.http.get()
}

Respuesta #1

Como señala bastante correctamente JB Nizet, los valores JSON deserializados que resultan de las solicitudes HTTP nunca serán instancias de una clase.

Aunque el doble rol (ver más abajo) del constructo class en TypeScript hace posible usar una class para describir la forma de estos valores de respuesta, es una mala práctica porque el texto de la respuesta se deserializará en objetos JavaScript simples.

Declaraciones de clases en JavaScript y TypeScript:

En JavaScript, una declaración de clase

class Comment {
  constructor(likes, comment) {
    this.likes = likes;
    this.comment = comment;
  }
}

crea un valor que puede ser instanciado usando new para actuar como lo que es esencialmente una fábrica.

En TypeScript, una declaración de clase crea dos cosas.

El primero es la misma clase de JavaScript value descrita arriba. El segundo es un tipo que describe la estructura de las instancias creadas al escribir

new Comment(4, 'I love your essays')

Ese segundo artefacto, el tipo, luego puede usarse como una anotación de tipo, como en tu ejemplo de

register(): Observable<Comment[]> {
    return this.http.get()
}

que dice que register devuelve un Observable de Arrays de instancias de la class Comment.

Ahora imagina que tu solicitud HTTP devuelve el siguiente 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"
  }
]

Sin embargo, la declaración del método

register(): Observable<Comment[]>;

mientras permite correctamente a los llamantes escribir

register().subscribe(comments => {
  for (const comment of comment) {
    if (comment.likes > 0) {
      likedComments.push(comment);
    }
  }
});

lo cual está bien y es bueno, desafortunadamente también permite a los llamantes escribir código como

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

Dado que los comentarios en realidad no son instancias de la clase Comment, la verificación anterior siempre fallará y, por lo tanto, hay un error en el código. Sin embargo, TypeScript no detectará el error porque dijimos que comments es un arreglo de instancias de la clase Comment, lo que haría que la verificación fuera válida (recuerda que response.json() devuelve any, que puede convertirse a cualquier tipo sin advertencias, por lo que todo parece estar bien en tiempo de compilación).

Si, sin embargo, hubiéramos declarado comment como una interface

interface Comment {
  comment: string;
  likes: number;
}

entonces getComments continuará haciendo la verificación de tipos, porque de hecho es código correcto, pero getTopComment generará un error en tiempo de compilación en la instrucción if porque, como han señalado muchos otros, una interface, al ser un constructo solo de tiempo de compilación, no puede ser usada como si fuera un constructor para realizar una verificación instanceof. El compilador nos indicará que tenemos un error.


Observaciones:

Además de todas las otras razones mencionadas, en mi opinión, cuando tienes algo que representa datos simples en JavaScript/TypeScript, usar una clase suele ser excesivo. Crea una función con un prototipo y tiene muchos otros aspectos que probablemente no necesitamos ni nos importan.

También desecha los beneficios que obtienes por defecto si usas objetos. Estos beneficios incluyen azúcar sintáctica para crear y copiar objetos y la inferencia de tipos de TypeScript para estos objetos.

Considerar

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

Si Comment es una interfaz y quiero usar el servicio anterior para crear un comentario, puedo hacerlo de la siguiente manera

import CommentService from 'app/comment-service';

export async function createComment(likes: number, comment: string) {    
  const commentService = new CommentService();
  
  await commentService.createCommnet({comment, likes});
}

Si Comment fuera una clase, necesitaría introducir algo de código repetitivo al requerir el import de Comment. Naturalmente, esto también incrementa el acoplamiento.

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

Eso son dos líneas extras, y una implica depender de otro módulo solo para crear un objeto.

Ahora, si Comment es una interfaz, pero quiero una clase sofisticada que haga validación y demás antes de dársela a mi servicio, aún puedo tener eso también.

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

En resumen, hay muchas razones para usar una interface para describir el tipo de las respuestas y también de las solicitudes que interactúan con un servicio HTTP cuando se usa TypeScript.

Nota: también puedes usar una declaración type, que es igualmente segura y robusta, pero es menos idiomática y las herramientas alrededor de interface a menudo la hacen preferible para este escenario.

Respuesta #2

Como en la mayoría de los otros lenguajes OOP: Para las clases puedes crear instancias (a través de su constructor), mientras que no puedes crear instancias de interfaces.

En otras palabras: Si solo devuelves JSON deserializado, entonces tiene sentido usar la interfaz para evitar confusión. Supongamos que agregas algún método foo a tu clase Comment. Si tu método register se declara para devolver un Comment, entonces podrías suponer que puedes llamar a foo sobre el valor devuelto por register. Pero esto no funcionará ya que lo que register efectivamente devuelve es solo JSON deserializado sin tu implementación de foo en la clase Comment. Más específicamente, NO es una instancia de tu clase Comment. Por supuesto, también podrías declarar accidentalmente el método foo en tu CommentInterface y aún así no funcionaría, pero entonces no habría código real para el método foo que simplemente no se está ejecutando, lo que hace más fácil razonar sobre la causa raíz de por qué tu llamada a foo no funciona.

Además, piénsalo a nivel semántico: Declarar que se devuelve una interfaz garantiza que todo lo que se declara en la interfaz esté presente en el valor devuelto. Declarar que se devuelve una instancia de clase garantiza que... bueno... devuelves una instancia de clase, lo cual no es lo que estás haciendo, ya que estás devolviendo JSON deserializado.

Artículos relacionados