#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