#typescript #typescript-generics
#typescript #typescript-дженерики
Вопрос:
Я пытаюсь сделать что-то вроде следующего, но это выдает ошибку компиляции / ввода.
interface IEntityLookup {
[Entity.PERSON]: IPersonLookup
[Entity.COMPANY]: ICompanyLookup
}
interface ISubEntity {
[Entity.PERSON]: People
[Entity.COMPANY]: Companies
}
function mapEntity<E extends Entity, S extends ISubEntity[E]>(
entityType: E,
subEntity: S
): IEntityLookup[E][S] | null {
switch (entityType) {
case Entity.PERSON:
return mapPerson(subEntity)
case Entity.Company:
return mapCompany(subEntity)
}
}
В частности, посмотрите на функцию mapEntity
, я хочу иметь возможность возвращать что-то вроде IEntityLookup[E][S]
: возможно ли это в Typescript?
Я оставляю здесь полные определения:
enum Companies {
TESLA = "tesla",
MICROSOFT = "microsoft",
}
interface ITesla {
id: string
cars: number
}
interface IMicrosoft {
id: string
software: string
}
enum People {
ELON_MUSK = "elon-musk",
BILL_GATES = "bill-gates",
}
interface IElonMusk {
id: string
rockets: number
}
interface IBillGates {
id: string
windows_version: string
}
interface ICompanyLookup {
[Companies.TESLA]: ITesla
[Companies.MICROSOFT]: IMicrosoft
}
interface IPersonLookup {
[People.ELON_MUSK]: IElonMusk
[People.BILL_GATES]: IBillGates
}
function mapPerson<T extends People>(
personType: T
): IPersonLookup[T] | null {
switch (personType) {
case People.ELON_MUSK:
return {id: "1", rockets: 1000} as IPersonLookup[T]
case People.BILL_GATES:
return {id: "1", windows_version: "98"} as IPersonLookup[T]
default:
return null
}
}
function mapCompany<T extends Companies>(
companyType: T
): ICompanyLookup[T] | null {
switch (companyType) {
case Companies.TESLA:
return {id: "1", cars: 1000} as ICompanyLookup[T]
case Companies.MICROSOFT:
return {id: "1", software: "98"} as ICompanyLookup[T]
default:
return null
}
}
enum Entity {
PERSON = "person",
COMPANY = "company",
}
interface IEntityLookup {
[Entity.PERSON]: IPersonLookup
[Entity.COMPANY]: ICompanyLookup
}
interface ISubEntity {
[Entity.PERSON]: People
[Entity.COMPANY]: Companies
}
function mapEntity<E extends Entity, S extends ISubEntity[E]>(
entityType: E,
subEntity: S
): IEntityLookup[E][S] | null {
switch (entityType) {
case Entity.PERSON:
return mapPerson(subEntity)
case Entity.Company:
return mapCompany(subEntity)
}
}
Ответ №1:
Возможно, это не имеет отношения к основному вопросу, но вы можете отказаться от as
утверждений в mapPerson
и mapEntity
, создав карту, а затем вернув значение из карты. Вам также не нужно null
, потому personType
что всегда есть a Person
, так что нет никаких шансов, что оно не будет найдено.
function mapPerson<T extends People>(
personType: T
): IPersonLookup[T] {
const map: IPersonLookup = {
[People.ELON_MUSK]: { id: "1", rockets: 1000 },
[People.BILL_GATES]: { id: "1", windows_version: "98" }
}
return map[personType];
}
Вы получаете ошибки mapEntity
, потому что, насколько это касается typescript, это не абсолютная гарантия совпадения сущности E
и элемента S
. Это ошибка, которую вы получаете при передаче subEntity
в качестве аргумента в mapCompany
— даже внутри case Entity.COMPANY
ветки.
Argument of type 'S' is not assignable to parameter of type 'Companies'.
Type 'ISubEntity[E]' is not assignable to type 'Companies'.
Type 'Companies | People' is not assignable to type 'Companies'.
E
может быть объединение обеих сущностей, и мы могли бы сделать что-то действительно глупое, подобное этому:
mapEntity<Entity, People>(Entity.COMPANY, People.BILL_GATES);
Что привело бы к тому, что мы вызывали mapCompany
бы с a Person
вместо a Company
.
Честно говоря, действительно сложно правильно вводить такие вещи. То, что я придумал, основано на объединении допустимых пар. Мы используем сопоставленный тип с двойным вложением для доступа к сущностям и вложенным объектам. Каждый из них индексируется keyof
, чтобы сгладить его.
type AllLookups = {
[E in keyof IEntityLookup]: {
[S in keyof IEntityLookup[E]]: {
entity: E;
subEntity: S;
value: IEntityLookup[E][S];
}
}[keyof IEntityLookup[E]]
}[keyof IEntityLookup]
Это соответствует типу объединения:
type AllLookups = {
entity: Entity.PERSON;
subEntity: People.ELON_MUSK;
value: IElonMusk;
} | {
entity: Entity.PERSON;
subEntity: People.BILL_GATES;
value: IBillGates;
} | {
entity: Entity.COMPANY;
subEntity: Companies.TESLA;
value: ITesla;
} | {
...;
}
Чтобы использовать это в нашей mapEntities
функции, мы делаем subEntity
S
основной универсальный. Вторая переменная T
извлекает члены объединения, которые имеют эту дочернюю сущность. Мы используем T
для получения типа сущности и возвращаемого типа.
Это дает нам очень четкую подпись с точки зрения вызова функции, где мы получаем точный возвращаемый тип и получаем ошибки, если тип сущности и тип элемента не совпадают.
// error -- good!
mapEntity(Entity.COMPANY, People.BILL_GATES);
// returns IBillGates
mapEntity(Entity.PERSON, People.BILL_GATES);
// returns ITesla
mapEntity(Entity.COMPANY, Companies.TESLA);
Но нам все равно нужно делать утверждения в теле функции, потому что сужение переменной не сужает универсальную, а сужение одной переменной не сужает другую. Так что я не знаю, действительно ли это значительное улучшение по сравнению с тем, что у вас было раньше. Возможно, было бы лучше, если entityType
бы и subEntity
были двумя свойствами одного и того же объекта, и switch
это было сделано без деструктурирования.
function mapEntity<S extends AllLookups['subEntity'], T extends AllLookups amp; { subEntity: S }>(
entityType: T['entity'], subEntity: S
): T['value'] {
switch (entityType) {
case Entity.PERSON:
return mapPerson(subEntity as People);
case Entity.COMPANY:
return mapCompany(subEntity as Companies);
default:
throw new Error("Invalid entity type " entityType);
}
}
Ссылка на игровую площадку Typescript
Что касается IEntityLookup[E][S]
, это ошибка, потому IEntityLookup[Entity]
что это объединение IPersonLookup | ICompanyLookup
, и вы не можете проиндексировать это объединение. Вам придется использовать сложное сопоставление, аналогичное для AllLookups
того, чтобы получить значение для дочерней сущности независимо от типа сущности.