Метод родительского класса Typescript возвращает изменение типа по дочернему свойству

#typescript #inheritance

#typescript #наследование

Вопрос:

Есть ли способ заставить метод, используемый родительским классом, возвращать точный тип (при изменении свойства, которое он использует для получения типа)?

Пример:

 abstract class A {
  a: string

  constructor(a: string) {
    this.a = a;
  }
}


class B extends A {
  b: string

  constructor(a: string, b: string) {
    super(a);
    this.b = b;
  }
}


abstract class AColl {
  abstract itemsMap: Map<string, A>

  items() {
    return [...this.itemsMap.values()];
  }
}

class BColl extends AColl {
  itemsMap: Map<string, B>
}

const bColl = new BColl();

bColl.items() -> Map<string, A> instead of Map<string,B>
 

В этом примере Intellisense считает, что это Map<string, A> вместо Map<string,B> , который я могу получить, хотя я думал, что он сможет вывести правильный тип (возможно, наивный с моей стороны).

Есть ли способ добиться этого без необходимости повторной реализации метода в дочернем классе?

Ответ №1:

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

 abstract class AColl<V extends A = A> {
    itemsMap: Map<string, V>; // no need to be abstract when class'es type param is used.

    items(): V[] {
        return [...this.itemsMap.values()];
    }
}

class BColl extends AColl<B> {
    // no need to have itemsMap field - extends AColl<B> does the job.
}

const bColl = new BColl();
bColl.items()
 

Комментарии:

1. Потрясающе! Большое спасибо. (Только начал экспериментировать с Typescript, поэтому не знал о параметризации — рассмотрим это немного подробнее)

2. Соответствующая ссылка на документацию: универсальные классы

Ответ №2:

Внутри тела AColl items метода тип

 [...this.itemsMap.values()]
 

с нетерпением оценивается Array<A> как . Когда вы начинаете искать свойства on this , компилятор заменяет A this . Такая быстрая замена обычно полезна, поскольку она дает определенные типы для вещей, и в противном случае компилятору пришлось бы отложить оценку практически чего угодно на случай, если кто-то заботится о подклассах с более конкретными типами. И такие отложенные типы, как правило, сложны в использовании; компилятор часто не может проверить возможность их назначения.

Однако в вашем случае this.itemsMap вычисляется значение type Map<string, A> , и ваши надежды рушатся; все подклассы AColl будут возвращать Array<A> for items() .


Вместо этого то, что вы хотите this , должно рассматриваться как «независимо this от того, что происходит в любом подклассе AColl , вызывающем этот метод». Такая концепция уже существует в TypeScript: «полиморфный this «; вы можете просто использовать this в качестве типа.

Вы можете думать о полиморфном this как о своего рода «неявном параметре универсального типа». То есть вы могли бы добиться аналогичного эффекта, создав AColl generic like AColl<T> , а затем сославшись на T него позже. Иногда можно использовать явные обобщения (и я вижу, что другой ответ здесь делает это), но они не всегда необходимы, когда вы хотите сказать «что бы это ни this было».

Итак, вы хотели бы this сохранить тип this , но компилятор охотно заменяет A его, как только вы начинаете индексировать в нем. Единственный способ избежать этого — использовать утверждение типа для возвращаемого значения items() . Вы бы утверждали, что возвращаемое значение имеет тип, который вы вычисляете, который зависит от this :

 items() {
    return [...this.itemsMap.values()] as
        Array<this["itemsMap"] extends Map<string, infer C> ? C : never>;
}
 

То есть использует типы поиска и вывод условного типа, чтобы сказать: «возьмите this , посмотрите на его itemsMap свойство, выясните, какой тип значений он хранит, и верните одно Array из них». Теперь это будет работать так, как вы хотите внутри BColl :

 const bColl = new BColl();
bColl.itemsMap.set("foo", new B("foo", "bar"))
console.log(bColl.items()[0].b.toUpperCase()); // BAR
 

Игровая площадка ссылка на код