Búsqueda de sitios web

TypeScript - Vue3 con solicitudes Axios fuertemente tipadas


Estoy tratando de entender algunos conceptos básicos de "seguridad de tipos" con Vue3, TypeScript y Axios.

Es algo bastante simple, ¡y siento como si me estuviera perdiendo de algo obvio!

He creado una interfaz Book:

Book.ts

interface Book {
  id: string;
  userId: string;
  title: string;
  content: string;
}

export default Book;

También he creado un servicio sencillo para obtener algo de JSON, por ejemplo.

  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  },

(Nota: El JSON tiene body en lugar de content como se define en la interfaz)

DataService.ts

import axios, { AxiosResponse } from "axios";
import Book from "@/interfaces/Book";

class DataService {
  async FetchBooks(): Promise<Book[]> {
    let response: Book[] = [
      { id: "1", userId: "someone", title: "A title", content: "Some content" }
    ];
    try {
      const val = await axios.get<Book[]>(
        `https://jsonplaceholder.typicode.com/posts`
      );

      if (val.data && val.status === 200) {
        response = val.data;
      }
    } catch (error) {
      console.log(error);
    }

    return response;
  }
}

export default new DataService();

Mi primera pregunta es, ¿por qué mi valor de respuesta del servicio todavía contiene "body" cuando no existe en la interfaz Book? Esperaba que se eliminara en algún punto, ya que estoy pasando el tipo Book a la solicitud get de axios.

Mi segunda pregunta es, ¿por qué se me permite hacer referencia a {{ book.body }} abajo, cuando de nuevo estoy manejando la respuesta como un Book y body no está definido en la interfaz?

Book.vue

<template>
  <div v-for="book in books" :key="book.id">
    <div class="mb-2">
      {{ book.body }}
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import DataService from "@/services/DataService";

// eslint-disable-next-line no-unused-vars
import Book from "@/interfaces/Book";

export default defineComponent({
  async setup() {
    let books: Book[] = [];

    await DataService.FetchBooks().then(data => (books = data));

    return { books };
  }
});
</script>

Respuesta #1

¿Por qué mi valor de respuesta del servicio todavía contiene "body" cuando no existe en la interfaz Book?

Porque en TypeScript, las interfaces son simplemente una forma de definir "contratos" para una entidad.

Dicho esto, un objeto puede tener más propiedades de las que están definidas en el contrato, pero el compilador solo verifica que al menos las requeridas estén presentes y coincidan con los tipos requeridos. Hay algunos casos en los que TypeScript no es tan flexible. Aprende más.

Esperaba que se eliminara en algún momento, ya que estoy pasando el tipo Book a la solicitud get de axios.

Así que no lo hará, a menos que redefinas los elementos de respuesta individuales omitiendo esa propiedad (el body) o renombrándola a content—entonces y solo entonces—el contrato funcionará de la mejor manera. Por ejemplo:

const val = await axios
  .get<Array<Book & { body: string }>>(
    `https://jsonplaceholder.typicode.com/posts`
  )
  .then(res => res.data.map(
    ({ body, ...props }) => Object.assign({ content: body }, props)
  ));

Para evitar saturar la afirmación de tipo, recomendaría agregar un DTO adicional para el objeto Book original, uno que incluya un tipo de intersección para la propiedad body "cruda". Por ejemplo:

interface IBook {
  id: string;
  userId: string;
  title: string;
  content: string;
}

interface IBookRaw extends Omit<IBook, 'content'> {
  body: string;
}

Con ese añadido, mejoremos el mapeo de datos anterior con la ayuda de una clase que implemente esta interfaz, por ejemplo:

class Book implements IBook {
  id: string;
  userId: string;
  title: string;
  content: string;

  constructor({ id, userId, title, body }: IBookRaw) {
    this.id = id;
    this.userId = userId;
    this.title = title;
    this.content = body;
  }
}

const val = await axios
  .get<IBookRaw[]>(
    `https://jsonplaceholder.typicode.com/posts`
  )
  .then(res =>
    res.data.map(item => new Book(item))
  );

¿Por qué se me permite referenciar {{ book.body }} a continuación, cuando nuevamente estoy manejando la respuesta como un Book y body no está definido en la interfaz?

Hablando sobre la interpolación de plantillas, si usas Vetur, necesitarás habilitar vetur.experimental.templateInterpolationService para obtener el beneficio de las sugerencias de linting. Una vez habilitado, no deberías poder compilar sin primero corregir el error de "propiedad faltante" en la plantilla (lo cual nuevamente consiste en renombrar la propiedad o eliminarla).

De lo contrario (sin el servicio de interpolación de plantillas de Vetur), no habrá verificación de tipos y, por supuesto, terminará evaluándose como undefined.

Artículos relacionados