TypeScript: отличить класс от экземпляра класса? - PullRequest
0 голосов
/ 05 мая 2020

В TypeScript у меня есть класс, который хранит коллекцию объектов.

class Collection<T extends Model> extends Array<T> {
  options: CollectionOptions;

  constructor(options: CollectionOptions) {
    super();

    this.options = options;
  }

  add(model: T): T {
    this.push(model);
    return model;
  }
}

У меня также есть механизм для создания новых коллекций:

function createCollection<T extends Model>(
  _ModelClass: T, options: CollectionOptions
): Collection<T> {
  const collection: Collection<T> = new Collection(options);

  return collection;
}

У меня есть несколько многоразовых , generi c options и очень базовый c класс модели:

type CollectionOptions = {
  key: string
}

class Model {
  [key: string]: any;
}

Теперь, когда я создаю конкретный класс, чтобы использовать его с:

class User extends Model {
  id: number = 0
}

Если я создаю экземпляр Коллекция выглядит так:

const model = createCollection(new User(), { key: 'id' });

Тогда это допустимо:

model.add(new User());

Но если я отправлю класс User вместо экземпляра:

const model = createCollection(User, { key: 'id' });

Тогда я получаю эту ошибку, когда я пытаюсь сделать model.add(new User()):

Аргумент типа «Пользователь» не может быть назначен параметру типа «typeof User». Свойство prototype отсутствует в типе User, но требуется в типе typeof User. Ts (2345)

Есть ли способ изменить мою функцию createCollection, чтобы прояснить это? что его возвращаемый тип - это коллекция экземпляров , а не классов ?

1 Ответ

1 голос
/ 05 мая 2020

Аргумент _ModelClass для createCollection полностью не используется внутри JavaScript, верно? Похоже, что его единственная цель - предоставить компилятору TypeScript сайт вывода для параметра generi c T. Если да, то, возможно, «правильным» решением будет полностью отказаться от этого аргумента:

function createCollection<T extends Model>(
    options: CollectionOptions
): Collection<T> {
    const collection: Collection<T> = new Collection(options);
    return collection;
}

и либо заставить вызывающих абонентов вручную указать тип параметра T, например, User:

const model = createCollection<User>({ key: 'id' }); // manually specified 
model.add(new User()); // okay

, или попросите их присвоить возвращаемое значение вручную аннотированной переменной типа, скажем, Collection<User>, и заставить компилятор вывести T как User контекстуально из желаемого типа возврата:

const model2: Collection<User> = createCollection({ key: 'id' });
model2.add(new User()); // okay

Если вы хотите сохранить фиктивный аргумент _ModelClass и ваше намерение состоит в том, чтобы передать класс конструктор вместо класса экземпляр , тогда createCollection() имеет неправильную подпись. Объявления классов, такие как ваш пример User, представляют как именованный тип интерфейса , соответствующий типу экземпляров класса, так и именованное значение , относящееся к конструктору класса. И они разные, несмотря на одинаковое название. Значение конструктора с именем User не относится к типу User.

Тип с именем User вроде как {id: number; [key: string]: any;}; объект со свойством numeri c id и множеством других возможных свойств, в то время как тип значения User (то есть typeof User,) вроде как new () => User; a newable объект, который создает значение типа User при вызове с new.

Если вы хотите узнать больше о различиях между типами и значениями и как одно и то же имя может относиться к типу и значению в зависимости от контекста, вы можете посмотреть другой мой длинный монолог на эту тему .

В любом случае, если ваше намерение чтобы принимать конструкторы, а не экземпляры, вы должны аннотировать тип параметра _ModelClass не как T, а как что-то вроде new (...args: any) => T, что означает «новый конструктор, который принимает некоторое количество аргументов, которые нам не нужны, и производит значение типа T ":

function createCollection<T extends Model>(
    _ModelClass: new (...args: any) => T,
    options: CollectionOptions
): Collection<T> {
    const collection: Collection<T> = new Collection(options);
    return collection;
}

Тогда ваша функция примет конструктор User:

const model = createCollection(User, { key: 'id' });
model.add(new User()); // okay

Но пожаловаться на экземпляр User:

const modelBad = createCollection(new User(), { key: 'id' }); // error!
// Type 'User' is not assignable to type 'new (...args: any) => Model`

Если по какой-то причине вы хотите, чтобы компилятор принимал либо экземпляр , либо конструктор как _ModelClass, вы можете аннотировать этот параметр как объединение инст Типы и конструктор:

function createCollection<T extends Model>(
    _ModelClass: (new (...args: any) => T) | T,
    options: CollectionOptions
): Collection<T> {
    const collection: Collection<T> = new Collection(options);
    return collection;
}

И тогда он будет работать в обоих направлениях:

const modelCtor = createCollection(User, { key: 'id' });
modelCtor.add(new User()); // okay

const modelInstance = createCollection(new User(), { key: 'id' }); // error!
modelInstance.add(new User()); // okay

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

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

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