У меня есть подобная цепочка наследования (вымышленные имена — я понимаю, что они не идеальны):
interface Vehicle {}
interface Airplane extends Vehicle {
number: number;
}
interface Boat extends Vehicle {
color: string;
}
interface Workshop<TVehicle extends Vehicle = Vehicle> {
vehicleType: TVehicle;
repair: (vehicle: TVehicle) => WorkOrder<this>;
}
interface WorkOrder<TWorkshop extends Workshop<TWorkshop["vehicleType"]>> {
vehicle: TWorkshop["vehicleType"];
}
Идея состоит в том, что тип WorkOrder.vehicle должен быть выведен на основе Workshop, предоставленного в качестве аргумента типа:
const orderA: WorkOrder<Workshop<Airplane>> = {
vehicle: {
number: 123,
},
};
const orderB: WorkOrder<Workshop<Boat>> = {
vehicle: {
color: "blue",
},
};
Это действительно работает! Я получаю завершение редактирования для приведенных выше примеров, и возникает ошибка:
const orderA: WorkOrder<Workshop<Airplane>> = {
vehicle: {
color: "blue", // ERROR! Object literal may only specify known properties, and 'color' does not exist in type 'Airplane'
},
};
НО - я получаю ошибку в аргументе типа TWorkshop["vehicleType"] внутри общего аргумента для WorkOrder:
Type 'TWorkshop["vehicleType"]' does not satisfy the constraint 'Vehicle'.ts(2344)
Я не понимаю, почему и почему ограничение на самом деле работает, учитывая жалобу - может ли кто-нибудь объяснить, что происходит?
Редактировать: Ссылка на TS Playground
@jcalz мне показалось это ошибкой (учитывая, что тип действительно работал так, как ожидалось, если предупреждение было проигнорировано), так что да, я бы принял это как ответ. Если у вас есть какие-либо сведения о безопасности использования @ts-ignore по этому поводу, мы будем очень признательны!
Сомневаюсь, что //@ts-ignore что-нибудь сломает, но обычно я этого избегаю. Я бы предпочел any, которое является намеренным ослаблением системы типов, в отличие от //@ts-ignore, которое представляет собой директиву «притвориться, что нет ошибок». Но это всего лишь мнение и, вероятно, выходит за рамки данной темы.
Я напишу ответ, когда у меня будет возможность, кстати






Изменить. Было отмечено, что исходный код работал в TS 4.7 и технически работает в TS 4.8, поэтому, хотя ответ ниже дает решение, он не касается этого конкретного синтаксиса.
interface WorkOrder<TWorkshop extends Workshop<TWorkshop["vehicleType"]>>
Вы пытаетесь использовать TWorkshop для ограничения TWorkshop, что является циклическим и, насколько мне известно, невозможно. Я подозреваю, что TS не выдает здесь хорошее сообщение об ошибке.
Мастерская инвариантна — она использует TVehicle в обычной позиции, где разрешен подкласс TVehicle, но не суперкласс. Он также использует TVehicle в качестве аргумента обратного вызова — аргумент функции должен быть таким же или более широким, как указанный тип — он может быть суперклассом, но не подклассом. Эти два требования означают, что Workshop<A> можно назначить только Workshop<A>. Обычно в такой ситуации ограничением будет TWorkshop extends Workshop<Vehicle>, но из-за инвариантности это не позволяет нам использовать TWorkshop<Boat> и бесполезно для нас.
Грубый ответ на инвариантные дженерики — просто использовать TWorkshop extends Workshop<any>, но это вводит в нашу систему нежелательный тип any. (В некоторых ситуациях это, вероятно, безвредно, например, const foo = x satisfies Y<any>, здесь any используется только при проверке на соответствие и не попадает в типы наших значений.)
Вместо этого мы можем просто сделать TVehicle extends Vehicle нашим общим типом для WorkOrder. В данном примере это тривиально, но даже если бы нам понадобился Workshop, мы могли бы создать подходящий тип мастерской с помощью Workshop<TVehicle>.
interface Vehicle {}
interface Airplane extends Vehicle {
number: number;
}
interface Boat extends Vehicle {
color: string;
}
interface Workshop<TVehicle extends Vehicle = Vehicle> {
vehicleType: TVehicle;
repair: (vehicle: TVehicle) => WorkOrder<TVehicle>;
}
interface WorkOrder<TVehicle extends Vehicle> {
vehicle: TVehicle;
// workshop: Workshop<TVehicle>; // for example
}
const orderA: WorkOrder<Airplane> = {
vehicle: {
number: 123,
},
};
const orderB: WorkOrder<Boat> = {
vehicle: {
color: "blue",
},
};
const orderC: WorkOrder<Airplane> = {
vehicle: {
color: "blue",
},
};
type WorkshopAirplane = Workshop<Airplane>
// we can get the Vehicle type from a Workshop type too
const orderD: WorkOrder<WorkshopAirplane['vehicleType']> = {
vehicle: {
number: 123,
},
};
Мне придется перечитать это завтра, но подразумеваемая (и реальная) проблема заключается в том, что мне нужно использовать информацию, передаваемую как дженерики для Workshop в WorkOrder, и я просто не могу иметь WorkOrder с Vehicle универсальным параметром. К сожалению, передача any также неприемлема, поскольку мне нужна информация о типе. Обратите внимание, что мои типы, как указано, похоже, работают так, как задумано, хотя я получаю вышеупомянутую ошибку в TS 4.8+ (но не 4.7).
Для будущих читателей: продолжение этой темы в машинописном дискорде. На данный момент новых ответов там не предложено.
Это похоже на ту же проблему, о которой сообщалось в microsoft/TypeScript#49490 , вызванную изменением microsoft/TypeScript#49119 , выпущенным в TypeScript 4.8 для улучшения сокращения типов пересечений . Неясно, считается ли это ошибкой, ограничением дизайна или работает так, как задумано. Ваше общее ограничение является циклическим, и поэтому вы не можете ожидать, что такие вещи будут работать так, как хотелось бы.
Это ответ на заданный вопрос. Но, вероятно, вы также хотите знать, как это исправить.
Вы можете заставить TypeScript увидеть, что ваш аргумент типа соответствует ограничению Vehicle, пересекая его с Vehicle:
interface WorkOrder<TWorkshop extends Workshop<TWorkshop["vehicleType"] & Vehicle>> {
vehicle: TWorkshop["vehicleType"];
}
Или вы можете просто использовать любой тип, чтобы сказать TS не волноваться... поскольку ограничение, которое вы применяете, не особенно полезно (конечно TWorkshop extends Workshop<TWorkshop["vehicleType"]>, это более или менее тавтология, поэтому вы не теряете во многом ослабив это до any):
interface WorkOrder<TWorkshop extends Workshop<any>> {
vehicle: TWorkshop["vehicleType"];
}
На самом деле, я бы почти наверняка использовал здесь any на практике, чтобы избежать цикличности и показать, что меня действительно не волнует, что там происходит, кроме своего рода Workshop. Я бы даже не подумал о попытке изобразить круговое самоограничение, если бы any не имело какого-нибудь действительно негативного побочного эффекта.
Детская площадка, ссылка на код
Большое спасибо, это очень полезно! Не знаю, почему я не учел Workshop<any> (вероятно, потому, что «любое — зло»), но это действительно изящное решение.
Я отметил, что вы также можете использовать any в моем ответе. Это приведет к типу TWorkshop["vehicleType"] как any в определении типа WorkOrder, поэтому, если вы попытаетесь выполнить какую-либо логику типа внутри самого типа the WorkOrder, вы потеряете здесь безопасность типов. Однако любой экземпляр WorkOrder включает в себя конкретную мастерскую, у которой нет any, поэтому это очень небольшая слабая сторона.
Это ms/TS#49490, но он не был классифицирован как ошибка или ограничение дизайна и даже не работает должным образом (и он не новый, так что кто знает, будет ли он когда-либо проверен и будет ли он когда-либо проверен). У вас есть циклическое ограничение, и TS не может его проверить. Вы бы приняли это как ответ? Или вам нужно просмотреть сортировку ms/TS#49490, прежде чем принять ответ?