Тип возвращаемого значения метода класса на основе типа параметра

У меня есть функция, которая принимает что угодно в качестве параметра, но если она получает function (с прототипом), она должна вернуть экземпляр этой функции (или класса, поскольку они являются функциями). Если нет, он может искать что-либо, назначенное этому ключу, внутри карты и возвращать его.

The super simplified code would look something like (playground):

// Go check service.get() method
const isConstructable = (fn: any) => typeof fn === 'function' && 'prototype' in fn;

class Animal {
  name: string | undefined
  constructor(name?: string){
    this.name = name;
  }
}

const service = new (class Service {
  map = new Map()

  set(key: any, value: any = key) {
    return this.map.set(key, value)
  }
  
  get<T>(key: { new (): T }): T {
    const ToResolve = this.map.get(key);
    if (isConstructable(ToResolve)) {
        return new ToResolve();
    }
    
    return ToResolve;
  }
})

service.set('bob', new Animal('Bob'))
service.set(Animal);

const genericAnimal = service.get(Animal)
const bob = service.get('bob') // Argument of type 'string' is not assignable to parameter of type 'new () => unknown'

console.log(genericAnimal instanceof Animal)
console.log(bob instanceof Animal)

Я читал Использование типов классов в обобщениях и нашел этот код

function create<T>(c: { new (): T }): T {
  return new c();
}

Это работает хорошо, но мне нужно, чтобы get(key) принимал не только конструкторы, но и любые примитивы, принимаемые Map, например:

const jane = {} // object reference, since TS won't let me use symbols
service.set(jane, class Jane extends Animal {
  constructor() {
    super('Jane')
  }
})
console.log(service.get(jane).name === 'Jane')

Таким образом, я получаю ошибки компиляции, такие как Argument of type 'string' is not assignable to parameter of type 'new () => unknown'

Также пробовал с: get<T>(key: T): T extends Function ? T : unknown, что, очевидно, не работает, но кажется таким близким :(

Должен ли я создавать определенный тип для параметра key? Дайте определение перегрузкам? Как?

заранее спасибо


person Frondor    schedule 01.11.2020    source источник
comment
Не могли бы вы объяснить, что ТС не разрешает мне использовать символы? const jane = Symbol("jane");" работает, верно? В чем проблема?   -  person jcalz    schedule 02.11.2020
comment
Кроме того, в этом примере service.get(jane) вернет значение типа unknown в нужном вам решении, верно? Таким образом, вы не можете написать service.get(jane).name, потому что unknown не имеет свойства name. Так что же вы действительно ищете?   -  person jcalz    schedule 02.11.2020
comment
@jcalz это не главная проблема. Я ищу правильный тип для service.get(). Я согласен с тем, что get("bob") возвращает неизвестное, пока TS не жалуется Argument of type 'string' is not assignable to parameter of type 'new () => unknown', И service.get(Animal) возвращает животное.   -  person Frondor    schedule 02.11.2020
comment
С Symbol другая история, я не должен был упоминать об этом. Давайте придерживаться параметров Function и String только для психического здоровья :)   -  person Frondor    schedule 02.11.2020


Ответы (1)


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

  get<T>(key: T): T extends new () => infer I ? I : unknown;
  get(key: any) {
    const ToResolve = this.map.get(key);
    if (isConstructable(ToResolve)) {
      return new ToResolve();
    }
    return ToResolve;
  }

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


Это работает по желанию для вашего первого набора примеров использования:

const genericAnimal = service.get(Animal) // Animal
const bob = service.get('bob') // unknown

но, конечно, метод set() типизирован настолько свободно, что нет никакой гарантии, что он будет использоваться правильно.

service.set(Animal, 1234); // no error? oops.

Я бы предложил изменить метод set(), чтобы отразить то, что происходит в методе get():

  set<T>(
    key: T, 
    ...args: T extends new () => infer I ? [value?: I] : 
      [value: unknown]
  ): void;
  set(key: any, value?: any) {
    this.map.set(key, typeof value === "undefined" ? key : value);
  }

Эта подпись может показаться пугающей, но в основном она говорит о том, что если key является функцией-конструктором, то вам разрешено вызывать set() либо с нулем, либо с одним дополнительным аргументом (кортеж аргументов типа [value?: I], где I — тип экземпляра класса), но если key не является функцией-конструктором, тогда вам нужно вызвать set() с дополнительным аргументом (кортеж аргументов типа [value: unknown].

И тогда вы получите ошибки компилятора, если кто-то попытается передать что-то странное с помощью конструктора:

service.set(Animal, 1234); // error!
service.set(Animal); // okay
service.set('bob', new Animal('Bob')) // okay

Наконец, я действительно не уверен, что делать с отслеживанием типов значений, вставленных для ключей, не являющихся конструкторами. Компилятор обрабатывает их как unknown, что означает, что если вы установите его и получите, вы, вероятно, потеряли информацию:

service.set("string", "hello");
console.log(service.get("string").toUpperCase()); // error at compile time
// but HELLO at runtime

Вам нужно либо использовать утверждения типов, либо защита типа для решения этой проблемы:

const str = service.get("string") as string; // I'm telling the compiler it's a string
console.log(str.toUpperCase()); // works now

const str2 = service.get("string");
console.log(
  typeof str2 === "string" ? str2.toUpperCase() : "NOT A STRING"
); // I'm testing

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


Playground link to code

person jcalz    schedule 02.11.2020
comment
Такой фантастический ответ! Это становится более понятным, когда я читаю некоторые термины, которые вы использовали для объяснения. Позвольте мне протестировать код, и я скоро свяжусь с вами! - person Frondor; 02.11.2020