Вот простой пример.
type Callback<T> = (sender: T) => void;
class Garage<T> {
private callbacks: Callback<T>[];
public constructor(callbacks: Callback<T>[]) {
this.callbacks = callbacks;
}
// Jobs in the garage are processed.
public update(sender: T): void {
for (const callback of this.callbacks) {
callback(sender);
}
}
}
class Vehicle {
public garage: Garage<Vehicle>; // You can also use Garage<this>, but this also generates errors.
public constructor(...callback: Callback<Vehicle>[]) {
this.garage = new Garage(callback);
}
public service(): void {
this.garage.update(this);
}
public startEngine(): void {}
}
class Car extends Vehicle {
public override garage: Garage<Car>; // You can also use Garage<this>, but this also generates errors.
public constructor(...callback: Callback<Car>[]) {
super(callback);
this.garage = new Garage(callback);
}
public openTrunk(): void {};
}
new Vehicle((sender) => {
sender.startEngine();
});
new Car((sender) => {
sender.openTrunk();
});
Я не понимаю, в чем здесь ошибка. Автомобиль получен из транспортного средства.
Я что-то упускаю или это универсальные классы, и TypeScript не может это правильно проверить?
Как могло бы выглядеть альтернативное решение, когда вы передаете соответствующий класс обратному вызову, и TypeScript отображает это правильно?
Ладно, звучит правдоподобно :-) Однако решения этой проблемы я пока не нашел. Простой <T extends Vehicle>
для обратного звонка и гаража не работает. Я бы предположил, что тогда станет ясно, что оба они происходят от «Гаража».
@Дай, я не думаю, что твое объяснение правильное. Это больше связано с контравариантностью типа параметра функции. Callback<Vehicle>
намеренно меняется в направлении, противоположном Vehicle
. Или другими словами, когда Car extends Vehicle
, то Callback<Vehicle> extends Callback<Car>
. TypeScript считает ковариантные типы параметров небезопасными. (Обратите внимание, это применимо только в том случае, если --strictFunctionTypes=true
)
• super(callback)
— это опечатка, верно? должно быть ...callback
? • Здесь действительно нужен полиморфизм this
, но без ms/TS#5863 это невозможно сделать напрямую, поскольку он, очевидно, нужен вам в аргументе конструктора. Я бы использовал явную количественную оценку, ограниченную F, как показано в этой ссылке на игровую площадку. Соответствует ли это вашим потребностям? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?
@jcalz Спасибо за этот пример. Выглядит хорошо. Меня смущает строка 26. public service(this: T): void
При вызове метода службы аргумент не требуется, если параметр равен this
. Ответ с объяснением был бы замечательным! Я также прочитаю вашу первую ссылку внимательнее.
Функции контравариантны по своим типам параметров (см. Разница между дисперсией, ковариацией, контравариантностью, бивариантностью и инвариантностью в TypeScript), что означает, что если T extends U
, то Callback<U> extends Callback<T>
, а не наоборот. Таким образом, концептуально Car
не является Vehicle
, поскольку, как вы это написали, у Vehicle
есть Garage
, который принимает все Vehicle
, а у Car
есть Garage
, который принимает только Car
.
Концептуально вы хотите использовать полиморфный этот тип ... по сути, это делает Vehicle
неявно универсальным , так что garage
имеет тип Garage<this>
, и где this
будет (обобщенный подтип) Car
внутри Car
. Тип this
— это неявная разновидность F-ограниченного обобщенного типа, где он действует как class Vehicle<this extends Vehicle<this>> {}
.
К сожалению, вы не можете написать это таким образом, потому что вы не можете использовать полиморфный this
в конструкторе. Для этого уже давно существует открытый запрос на функцию microsoft/TypeScript#5863, но на данный момент это не является частью языка.
Вместо этого вы могли бы сделать (кроме простого использования any
и не беспокоиться об этом или утверждений типа или тому подобного) — сделать Vehicle
явно универсальным, F-ограниченным способом:
class Vehicle<T extends Vehicle<T>> {
public garage: Garage<T>;
public constructor(...callback: Callback<T>[]) {
this.garage = new Garage(callback);
}
public service(this: T): void {
this.garage.update(this);
}
public startEngine(): void { }
}
а потом
class Car extends Vehicle<Car> { ⋯ }
Единственная дополнительная сложность заключается в том, что TypeScript не понимает, что значение с именем this
имеет тип T
(это то, что вы можете получить бесплатно с типами this
). Итак, внутри service()
по умолчанию вы получаете сообщение об ошибке this.garage.update(this)
. Поэтому вам нужно сообщить ему, что this
имеет тип T
, либо написав this.garage.update(this as T)
, либо вам нужно сообщить ему, что service()
можно вызывать только для объектов, где this
имеет тип T
, используя этот параметр. Это this: T
внутри public service(this: T): void
. Это виртуальный параметр, вы не передаете его в качестве аргумента. Он просто сообщает TypeScript, что foo.service()
разрешено вызывать только в том случае, если foo
имеет тип T
, который мы знаем extends Vehicle<T>
.
И теперь все должно работать как положено:
const c = new Car((sender) => {
sender.openTrunk();
});
c.garage.update(c); // okay, c is a Car
Хотя вы почти наверняка никогда не захотите использовать new Vehicle
сам по себе, так как из него получится какой-то безумный бесполезный шрифт:
const v = new Vehicle((sender) => {
sender.startEngine();
});
//const v: Vehicle<Vehicle<unknown>> // <-- wha?
Если вам что-то нужно, вы можете определить рекурсивный BaseVehicle
тип и использовать его как по умолчанию:
type BaseVehicle = Vehicle<BaseVehicle>;
class Vehicle<T extends Vehicle<T> = BaseVehicle> {⋯}
Но я бы сказал, что лучше просто забыть о создании экземпляров Vehicle
и использовать только конкретные подклассы.
Детская площадка, ссылка на код
Что касается вашего последнего предложения, в этом примере я бы превратил class Vehicle
в abstract class Vehicle
, так что вам в любом случае придется использовать производный класс. Спасибо за подробное объяснение.
«
Car
происходит отVehicle
». - Да, ноCallback<Car>[]
не является производным отCallback<Vehicle[]>
, поэтомуGarage<Vehicle>
не является супертипомGarage<Car>
.