Вот простой пример.
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>.