Búsqueda de sitios web

json - Deserialización con seguridad de tipos en TypeScript


Tengo este código de TypeScript de ejemplo que se supone que debe deserializar un JSON simple en una instancia de la clase Person y luego llamar al método foo en él, pero no funciona:

class Person {
    name!: string;
    age!: number;

    foo() {
        console.log("Hey!");
    }
}

fetch("/api/data")
    .then(response => {
        return response.json() as Promise<Person>;
    }).then((data) => {
        console.log(data);
        data.foo();
    });

La salida de la consola muestra que el objeto tiene una forma adecuada, pero no es reconocido como Persona:

Object { name: "Peter", age: 44 }

age: 44
name: "Peter"

Así que cuando intenta llamar al método foo falla:

Excepción no capturada (en promesa) TypeError: data.foo no es una función

http://127.0.0.1:8000/app.js:14
devolución de llamada de promesa* http://127.0.0.1:8000/app.js:12

¿Cómo puedo solucionarlo? ¿Debería usar Object.assign o hay otra solución mejor/nativa?

let x = (<any>Object).assign(Object.create(Person.prototype), data);
x.foo();

Respuesta #1

Recuerda, TypeScript es solo una forma de anotar el código JavaScript con verificadores de tipo. No hace nada extra. Por ejemplo, decir que el objeto devuelto por response.json() debe ser tratado como un Promise<Person> no significa que se invocará el constructor de tu clase Person. Más bien, solo te quedarás con un objeto JavaScript simple que tiene un nombre y una edad.

Me parece que necesitarás crear un constructor para tu clase Person que pueda crear una nueva instancia de una Persona basada en un objeto que coincida con su interfaz. ¿Algo así, quizás?

interface PersonLike {
    name: string;
    age: string;
}

class Person implements PersonLike {
    constructor(data: PersonLike) {
        this.name = data.name;
        this.age = data.age;
    }

    name: string;
    age: string;

    foo() {
        console.log("Hey!");
    }
}

fetch("/api/data")
    .then(response => {
        return response.json() as Promise<PersonLike>;
    }).then((data) => {
        const person = new Person(data);

        person.foo();
    });

También recomendaría usar un guardia de tipo en lugar de la palabra clave as, en caso de que la API de la que estás obteniendo datos cambie. Algo así, quizás:

function isPersonLike(data: any): data is PersonLike {
    return typeof data?.name === 'string' && data?.age === 'string';
}

fetch("/api/data")
    .then(response => {
        return response.json();
    }).then((data: unknown) => {
        if (isPersonLike(data)) {
            const person = new Person(data);

            person.foo();
        }
    });

Respuesta #2

... se supone que debe deserializar un JSON simple en una instancia de la clase Persona y luego ...

Desafortunadamente, el tipo genérico en TypeScript solo funciona como una especie de asistente de diseño de modelos. Nunca se compilará en un archivo JavaScript. Toma tu código de "fetch" como ejemplo:

fetch("/api/data")
    .then(response => {
        return response.json() as Promise<Person>;
    }).then((data) => {
        console.log(data);
        data.foo();
    });

Después de compilar el archivo TypeScript anterior en JavaScript, podemos ver que el código as Promise<Person> se elimina por completo:

fetch("/api/data")
    .then(function (response) {
    return response.json();
}).then(function (data) {
    console.log(data);
    data.foo();
});

Para implementar la "deserialización segura de tipos", necesitas guardar la información de clase/prototipo durante la serialización. De lo contrario, esta información de clase/prototipo se perderá.

... o ¿hay otra solución mejor/nativa? ... Por cierto, ¿qué pasa si un campo de clase tiene un tipo personalizado, es decir, es una instancia de otra clase?

No, no hay una solución nativa, pero puedes implementar serialización/deserialización 'tipo seguro' con algunas bibliotecas.

He creado un módulo de npm llamado esserializer para resolver este problema automáticamente: guardar los valores de instancias de clases de JavaScript durante la serialización, en formato JSON plano, junto con la información del nombre de la clase. Más tarde, durante la fase de deserialización (posiblemente en otro proceso o en otra máquina), esserializer puede deserializar recursivamente la instancia del objeto, conservando toda la información de Clase/Propiedad/Método, usando la misma definición de clase. Para tu caso de código "fetch", se vería así:

// Node.js server side, serialization happens here.
const ESSerializer = require('esserializer');
router.get('/api/data', (req, res) => {
  // ...
  res.json(ESSerializer.serialize(anInstanceOfPerson));
});

// Client side, deserialization happens here.
const ESSerializer = require('esserializer');
fetch("/api/data")
    .then(response => {
        return response.text() as Promise<string>;
    }).then((data) => {
        const person = ESSerializer.deserialize(data, [Person, CustomType1, CustomType2]);
        console.log(person);

        person.foo();
    });

Artículos relacionados