Как сделать экземпляр класса конструируемым в TypeScript? - PullRequest
0 голосов
/ 12 апреля 2020

Я пытаюсь преобразовать этот пакет в TypeScript без каких-либо серьезных изменений. У меня есть следующий код в TypeScript.

// DocumentCarrier.ts
/* export */ class DocumentCarrier {
    internalObject: {};
    model: Model;
    save: (this: DocumentCarrier) => void;

    constructor(model: Model, object: {}) {
        this.internalObject = object;
        this.model = model;
    }
}
DocumentCarrier.prototype.save = function(this: DocumentCarrier): void {
    console.log(`Saved document ${JSON.stringify(this.model)} to ${this.model.myName}`);
};

// Model.ts
// import {DocumentCarrier} from "./DocumentCarrier.ts";
/* export */class Model {
    myName: string;
    Document: typeof DocumentCarrier;
    get: (id: number) => void;

    constructor(name: string) {
        this.myName = name;

        const self: Model = this;
        class Document extends DocumentCarrier {
            static Model: Model;

            constructor(object: {}) {
                super(self, object);
            }
        }
        Document.Model = self;

        Object.keys(Object.getPrototypeOf(this)).forEach((key) => {
            Document[key] = this[key].bind(this);
        });

        this.Document = Document;

        return this.Document as any;
    }
}
Model.prototype.get = function(id: number): void {
    console.log(`Retrieving item with id = ${id}`);
}

// Usage
// index.ts
// import {Model} from "./Model.ts";
const User = new Model("User");
const user = new User({"id": 5, "name": "Bob"});
user.save(); // "Saved document {"id": 5, "name": "Bob"} to User"
console.log(User.Model.myName); // "User"
// console.log(User.myName); // "User" // This option would be even better, but isn't supported in the existing code
User.get(5); // "Retrieving item with id = 5"

В разделе Usage (в самом низу примера кода выше) я получаю несколько ошибок в TypeScript. Но выполнение этого кода в файле JavaScript работает отлично. Так что я знаю, что это работает, и код точен.

Я думаю, что самая большая проблема в том, что я пытаюсь сделать, это return this.Document as any. TypeScript интерпретирует это как приведение this.Document к экземпляру Model, хотя в действительности это не так.


У меня такой вопрос. В TypeScript, как я могу установить его, где вы можете запустить new MyClassInstance() и заставить его возвращать экземпляр другого класса? Это имеет двунаправленную ссылку от MyClassInstance и другой класс. Короче говоря, как мне заставить работать следующий код?

Важно, чтобы любое решение работало с разделом Usage, и в этот раздел не было внесено никаких изменений. За исключением раздела User.Model.myName против User.myName, который предпочтителен как User.myName, но в существующей версии функционирует как User.Model.myName.


Для простоты использования я также создал TypeScript Playground .

Ответы [ 2 ]

0 голосов
/ 12 апреля 2020

Я собираюсь интерпретировать этот вопрос строго как "как я могу дать типизацию существующему коду, чтобы компилятор понимал код в разделе Usage?" Таким образом, ответ не должен касаться выдаваемого JavaScript, но вместо этого должен изменять только определения типов, аннотации и утверждения.

В сторону: более общий вопрос «как мне реализовать класс, экземпляры которого сами являются конструкторами классов» - я не буду пытаться ответить, так как из моих исследований лучший ответ здесь - «не пытайтесь сделать это ", поскольку он плохо работает с прототипом модели наследования в JS. Вместо этого я бы предпочел, чтобы экземпляр неконструктивного класса содержал свойство, являющееся конструктором нового класса. Что-то вроде этого кода детской площадки . Я ожидаю, что вы были бы намного счастливее в долгосрочной перспективе.

Возвращаясь к типам: основная проблема здесь в том, что TypeScript не может указать, что конструктор класса возвращает тип, отличный от класса, являющегося классом. определены. Это либо намеренно (см. microsoft / TypeScript # 11588 , либо отсутствует функция (см. microsoft / TypeScript # 27594 ), но в любом случае это не является частью языка.

Что мы можем сделать здесь, это использовать объявление слияния . Когда вы пишете class Model {}, вы вводите как объект конструктора класса с именем Model, так и тип интерфейса с именем Model Этот интерфейс можно объединить, добавив методы и свойства, о которых компилятор еще не знает. В вашем случае вы можете сделать это:

interface Model {
    new(object: {}): DocumentCarrier;
    Model: Model;
}

Это позволит компилятору узнать, что Model экземпляров, помимо того, что свойства / методы объявлены в классе, также имеется свойство Model, тип которого Model, и, что важно, сигнатура конструктора. Этого достаточно, чтобы следующий код компилировался без ошибок:

const User = new Model("User");
const user = new User({ "id": 5, "name": "Bob" });
user.save(); // "Saved document {"id": 5, "name": "Bob"} to User"
console.log(User.Model.myName); // "User"
User.get(5); // "Retrieving item with id = 5"

Компилятор считает, что User.myName существует, чего нет во время выполнения, но это уже проблема с существующим кодом, поэтому я не буду касаться этого здесь , Можно дополнительно изменить типизацию, чтобы компилятор знал, что User.Model.myName существует и что User.myName не существует, но это становится довольно сложным, поскольку требуется разделить интерфейс Model на несколько типов, которые вы тщательно присваиваете на правильные значения. Поэтому сейчас я игнорирую это.

Единственное другое изменение, которое я здесь сделаю, - это добавление различных типов к реализации Model, например:

class Model {
    myName: string;
    Document: Model;
    get!: (id: number) => void;

    constructor(name: string) {
        this.myName = name;

        const self: Model = this;
        class Document extends DocumentCarrier {
            static Model: Model;

            constructor(object: {}) {
                super(self, object);
            }
        }
        Document.Model = self;

        (Object.keys(Object.getPrototypeOf(this)) as
            Array<keyof typeof DocumentCarrier>).forEach((key) => {
                Document[key] = this[key].bind(this);
            });

        this.Document = Document as Model;

        return this.Document;
    }
}

Единственное, что компилятор не сможет проверить в вышеприведенном, это то, что класс Document является допустимым Model, поэтому мы используем утверждение Document as Model. Кроме этого, я просто поставил несколько утверждений (get - это , определенно назначенный , а Object.keys() вернет массив ключей конструктора DocumentCarrier), чтобы вам не нужно было отключать --strict флаг компилятора.


Хорошо, надеюсь, это поможет. Удачи!

Детская площадка ссылка на код

0 голосов
/ 12 апреля 2020

После небольшого роуминга я кое-что получил.

Typescript жалуется на ваше решение, потому что, даже если вы возвращаете класс Document внутри конструктора Model, компилятор ожидает экземпляр Model , который не конструируем.

Итак, нам нужно сделать Model конструируемым. Фактически, то же самое, что создание функции, которая возвращает экземпляры чего-либо.
Сначала давайте объявим ваш класс DocumentCarrier. Теперь DocumentCarrier будет иметь два свойства, model и name (это был ранее введенный вами myName класс Model).

class DocumentCarrier {
    name: string = ``;
    constructor(public model: {}) { }
    save = () => console.log(`Saved document ${JSON.stringify(this.model)} to ${this.name}`)
}

После этого нам нужно объявление этой функции который возвращает модель экземпляра типа DocumentCarrier.

const Model = (name: string) => {
    return class extends DocumentCarrier {
        name: string = name;

        constructor(model: any) {
            super(model);
        }
    }
}

Функция принимает параметр string и возвращает конструктор типа DocumentCarrier, который принимает объект any в своем конструкторе и передает его DocumentCarrier конструктор.

Мы можем назвать Model следующим образом.

const User = Model('User');
const user = new User({id: 5, name: 'Bob'});
user.save(); // Saved document {"id":5,"name":"Bob"} to User

Вызов Model - единственное сделанное изменение. Теперь для вызова Model не нужно ключевое слово new.
С другой стороны, name в DocumentCarrier можно получить из последнего созданного экземпляра.


Кроме того, это решение могло бы быть немного более обобщенным c, определяя обобщенные c типы.

type Constructor<T> = new (...args: any[]) => T;

Тип Constructor ограничивает функцию конструктора любого типа T.

Теперь нам нужно переписать функцию Model:

const Model = <T extends Constructor<{}>>(Type: T, name: string) => {
    return class extends Type {
        name: string = name;
        constructor(...args: any[]) {
            super(...args);
        }
    }
}

Model теперь ожидает ту же строку, что и раньше, но дополнительный параметр - это тип, который мы хотим создать, поэтому

const User = Model(DocumentCarrier, 'User');
const user = new User({id: 5, name: 'Bob'});
user.save(); // Saved document {"id":5,"name":"Bob"} to User

Более того, поскольку мы выполняем свойство name, которое принадлежит экземпляру, который мы создаем внутри функции Model, мы должны ограничить тип ввода, чтобы ожидать, что свойство name.

type Constructor<T> = new (...args: any[]) => T;
interface DynamicallyConstructed {
    name: string;
}

class DocumentCarrier implements DynamicallyConstructed {
    name: string = ``;
    constructor(public model: {}) { }
    save = () => console.log(`Saved document ${JSON.stringify(this.model)} to ${this.name}`)
}

const Model = <T extends Constructor<DynamicallyConstructed>>(Type: T, name: string) => {
    return class extends Type {
        name: string = name;
        constructor(...args: any[]) {
            super(...args);
        }
    }
}

const User = Model(DocumentCarrier, 'User');
const user = new User({id: 5, name: 'Bob'});
user.save();

Надеюсь, это немного поможет.
Пожалуйста, прокомментируйте любую проблему.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...