Как выполнять литье в стиле runtime в TypeScript?

В настоящее время я работаю над проектом typescript, и мне действительно нравится вывод типа, который typescript приводит в таблицу. Тем не менее - при получении объектов из HTTP-вызовов - я могу отнести их к нужному типу, получить завершение кода и функции вызова на них время компиляции, но это приводит к ошибкам выполнения

Пример:

class Person{
    name: string;

    public giveName() {
        return this.name;
    }

    constructor(json: any) {
        this.name = json.name;
    }
}

var somejson = { 'name' : 'John' }; // Typically from AJAX call
var john = <Person>(somejson);      // The cast

console.log(john.name);       // 'John'
console.log(john.giveName()); // 'undefined is not a function'

Хотя это компилируется красиво - и intellisense предлагает мне использовать эту функцию, она дает исключение во время выполнения. Решением для этого может быть:

var somejson = { 'name' : 'Ann' };
var ann = new Person(somejson);

console.log(ann.name);        // 'Ann'
console.log(ann.giveName());  // 'Ann'

Но это потребует от меня создания конструкторов для всех моих типов. В paticular, имея дело с древовидными типами и/или с коллекциями, входящими в AJAX-вызов, нужно было бы перебрать все элементы и новый экземпляр для каждого.

Итак, мой вопрос: есть ли более элегантный способ сделать это? То есть, приведение к типу и наличие прототипических функций для него немедленно?

Ответ 1

Взгляните на скомпилированный JavaScript, и вы увидите, что утверждение типа (casting) исчезает, потому что оно только для компиляции. Прямо сейчас вы сообщаете компилятору, что объект somejson имеет тип Person. Компилятор верит вам, но в этом случае это не так.

Таким образом, эта проблема является проблемой JavaScript во время выполнения.

Основная цель, чтобы заставить это работать, - как-то сказать JavaScript, какова взаимосвязь между классами. Так что...

  • Найдите способ описания отношений между классами.
  • Создайте что-нибудь, чтобы автоматически сопоставлять json с классами на основе этих данных отношений.

Там много способов решить это, но я приведу один пример с головы. Это должно помочь описать, что нужно сделать.

Скажем, у нас есть этот класс:

class Person {
    name: string;
    child: Person;

    public giveName() {
        return this.name;
    }
}

И эти данные json:

{ 
    name: 'John', 
    child: {
        name: 'Sarah',
        child: {
            name: 'Jacob'
        }
    }
}

Чтобы отобразить это автоматически как экземпляры Person, нам нужно сообщить JavaScript, как связаны эти типы. Мы не можем использовать информацию типа TypeScript, потому что мы потеряем ее после ее компиляции. Одним из способов сделать это является наличие статического свойства в типе, который описывает это. Например:

class Person {
    static relationships = {
        child: Person
    };

    name: string;
    child: Person;

    public giveName() {
        return this.name;
    }
}

Затем здесь приведен пример функции многократного использования, которая обрабатывает создание объектов для нас на основе данных отношений:

function createInstanceFromJson<T>(objType: { new(): T; }, json: any) {
    const newObj = new objType();
    const relationships = objType["relationships"] || {};

    for (const prop in json) {
        if (json.hasOwnProperty(prop)) {
            if (newObj[prop] == null) {
                if (relationships[prop] == null) {
                    newObj[prop] = json[prop];
                }
                else {
                    newObj[prop] = createInstanceFromJson(relationships[prop], json[prop]);
                }
            }
            else {
                console.warn(`Property ${prop} not set because it already existed on the object.`);
            }
        }
    }

    return newObj;
}

Теперь будет работать следующий код:

const someJson = { 
        name: 'John', 
        child: {
            name: 'Sarah',
            child: {
                name: 'Jacob'
            }
        }
    };
const person = createInstanceFromJson(Person, someJson);

console.log(person.giveName());             // John
console.log(person.child.giveName());       // Sarah
console.log(person.child.child.giveName()); // Jacob

Playground

В идеале лучший способ - использовать что-то, что действительно читает код TypeScript и создает объект, который поддерживает отношения между классами. Таким образом, нам не нужно вручную поддерживать отношения и беспокоиться о изменениях кода. Например, прямо сейчас код рефакторинга немного рискован с этой настройкой. Я не уверен, что в данный момент существует нечто подобное, но это определенно возможно.

Альтернативное решение

Я только понял, что уже ответил на аналогичный вопрос с немного другим решением (хотя это и не связано с вложенными данными). Вы можете прочитать его здесь для некоторых идей:

экземпляр класса JSON для TypeScript?

Ответ 2

Прототип класса может динамически воздействовать на объект:

function cast<T>(obj: any, cl: { new(...args): T }): T {
  obj.__proto__ = cl.prototype;
  return obj;
}

var john = cast(/* somejson */, Person);

Смотрите документацию __proto__ здесь.

Ответ 3

Вы можете использовать Object.assign, например:

var somejson = { 'name' : 'Ann' };
var ann = Object.assign(new Person, somejson);

console.log(ann.name);        // 'Ann'
console.log(ann.giveName());  // 'Ann'

Но если у вас есть вложенные классы, вы должны отобразить объект throw и назначить его для каждого элемента.