Универсальная функция, которая возвращает IEntity[E][S]: Дженерики на два уровня ниже

#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 того, чтобы получить значение для дочерней сущности независимо от типа сущности.