Почему TypeScript не может переопределить метод родительского класса производным типом?

Вот простой пример.

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 отображает это правильно?

«Car происходит от Vehicle». - Да, но Callback<Car>[] не является производным от Callback<Vehicle[]>, поэтому Garage<Vehicle> не является супертипом Garage<Car>.

Dai 14.06.2024 10:50

Ладно, звучит правдоподобно :-) Однако решения этой проблемы я пока не нашел. Простой <T extends Vehicle> для обратного звонка и гаража не работает. Я бы предположил, что тогда станет ясно, что оба они происходят от «Гаража».

Pascal 14.06.2024 12:13

@Дай, я не думаю, что твое объяснение правильное. Это больше связано с контравариантностью типа параметра функции. Callback<Vehicle> намеренно меняется в направлении, противоположном Vehicle. Или другими словами, когда Car extends Vehicle, то Callback<Vehicle> extends Callback<Car>. TypeScript считает ковариантные типы параметров небезопасными. (Обратите внимание, это применимо только в том случае, если --strictFunctionTypes=true)

Behemoth 14.06.2024 14:43

super(callback) — это опечатка, верно? должно быть ...callback? • Здесь действительно нужен полиморфизм this, но без ms/TS#5863 это невозможно сделать напрямую, поскольку он, очевидно, нужен вам в аргументе конструктора. Я бы использовал явную количественную оценку, ограниченную F, как показано в этой ссылке на игровую площадку. Соответствует ли это вашим потребностям? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?

jcalz 14.06.2024 14:58

@jcalz Спасибо за этот пример. Выглядит хорошо. Меня смущает строка 26. public service(this: T): void При вызове метода службы аргумент не требуется, если параметр равен this. Ответ с объяснением был бы замечательным! Я также прочитаю вашу первую ссылку внимательнее.

Pascal 14.06.2024 16:37
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
5
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Функции контравариантны по своим типам параметров (см. Разница между дисперсией, ковариацией, контравариантностью, бивариантностью и инвариантностью в 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, так что вам в любом случае придется использовать производный класс. Спасибо за подробное объяснение.

Pascal 16.06.2024 10:26

Другие вопросы по теме