A generalized function that provides IEntity[E][S] as its return type, delving into two levels of

I've encountered a compilation/type error while attempting to implement something similar to the code snippet below.

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)
    }
}

The area of interest is in the mapEntity function where I aim to return IEntityLookup[E][S]. Is achieving this possible in Typescript?

Below are the complete definitions:

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)
    }
}

Answer №1

It may not be directly related to the main question, but a more efficient approach in mapPerson and mapEntity would be to utilize a map for value retrieval instead of using as assertions. The use of null in this case is unnecessary since personType will always be of type 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];
}

The errors occurring in mapEntity stem from TypeScript considerations that there is no absolute guarantee of match between entity E and subEntity S. This issue also arises when passing subEntity as an argument to mapCompany, especially within the case Entity.COMPANY branch.

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'.

One possible solution involves treating E as a union of entities, leading to potential errors such as calling mapCompany with a Person instead of a Company.

Addressing these challenges requires complex typing structures. By leveraging double-nested mapped types indexed by keyof, we access entities and sub-entities for pairing validations.

type AllLookups = {
    [E in keyof IEntityLookup]: {
        [S in keyof IEntityLookup[E]]: {
            entity: E;
            subEntity: S;
            value: IEntityLookup[E][S];
        }
    }[keyof IEntityLookup[E]]
}[keyof IEntityLookup]

This results in a union type:

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;
} | {
    ...;
}

In utilizing this structure within mapEntities function, primary generics are assigned to subEntity S; while the second variable T handles matching pairs for precise return types and error-checking for entity-subEntity mismatches.

This implementation streamlines function calls, providing accurate return types and detecting any inconsistencies between entity and sub-entity types.

// error -- good!
mapEntity(Entity.COMPANY, People.BILL_GATES);
// returns IBillGates
mapEntity(Entity.PERSON, People.BILL_GATES);
// returns ITesla
mapEntity(Entity.COMPANY, Companies.TESLA);

Despite these advancements, assertions within the function body remain necessary as narrowing one variable does not automatically narrow the generic counterparts. Thus, further optimization may be achieved by reorganizing object properties and restructuring the switch without destructuring.

function mapEntity<S extends AllLookups['subEntity'], T extends AllLookups & { 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 Playground Link

With respect to IEntityLookup[E][S], the error arises due to indexing a union like

IEntityLookup[Entity]</code which necessitates a sophisticated mapping akin to <code>AllLookups
for retrieving sub-entity values irrespective of entity type relationships.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

showing javascript strings on separate lines

I need assistance with displaying an array value in a frontend Angular application. How can I insert spaces between strings and show them on two separate lines? x: any = [] x[{info: "test" + ',' + "tested"}] // Instead of showing test , teste ...

The TypeScript Mongoose function is not available for type <Context = any>

How can I implement a schema method in Mongoose and TypeScript to compare passwords? interface IUser extends Document { email: string; username: string; password: string; role: string; comparePassword( candidatePassword: string, next: Nex ...

Properties of untyped objects in TypeScript are not defined

Here is the code snippet I've been working on: file.js const channel = {}, arr = [string,string,string]; for(let i = 0;i < arr.length;i++ ){ channel[arr[i]] = "Amo" //equal string value } I have an array that contains only string values, for ...

Calculating the length of an Observable in an Angular 4 MEAN stack application

In my Angular 4 application, I utilize a service function to fetch data from the MongoDB/Express.js backend: getArticles() { return this.http.get('api/articles').map(res => res.json()); } Is there a way for me to determine the length of th ...

ngx-datatables in Angular is experiencing issues with its filtering options

Having trouble implementing a filter option in ngx-datatables for all columns. I've written the code but it's not functioning correctly. Can anyone help me identify where I went wrong and find solutions? app.component.html: <label> Nam ...

Styling can be compromised by Angular due to selector element interference

I have a question regarding a bootstrap button group. The buttons within it are Angular components structured like this: <div class="btn-group float-right" role="group" aria-label="Basic example"> <app-action [actionType]="'inv ...

The quantity of documents queried does not align with the number of data counts retrieved from Firestore

Facing an issue with calculating the Firestore read count as it keeps increasing rapidly even with only around 15 user documents. The count surges by 100 with each page reload and sometimes increases on its own without any action from me. Could this be due ...

Transforming API response data into a Typescript object using React mapping

When I make an API call, the response data is structured like this: [ { "code": "AF", "name": "Afghanistan" }, { "code": "AX", "name": "Aland Islands" } ...

Construct a table utilizing Angular's ngFor directive that includes two rows, with the first row containing a single value from an array of objects and the second row displaying the remaining values

In my new project, I am utilizing angular. I am curious about how to optimize this code snippet with a more elegant approach using just one *ngFor directive. <table> <tr> <th *ngFor="let distribution of monthDistribution ">{{dis ...

Sharing information from Directive to Component in Angular

Here is a special directive that detects key strokes on any component: import { Directive, HostListener } from '@angular/core'; @Directive({ selector: '[keyCatcher]' }) export class keyCatcher { @HostListener('document:keydo ...

Has the antd Form.create() method been substituted with something else?

I have been working on creating a login page in react using antd. I came across a tutorial that seems to be outdated as it is giving me an error. After researching on a forum, I found out that form.create() is no longer available, but I am unsure of how to ...

Encountering problem with React Typescript fetching data from Spring Data REST API: the error message "Property '_embedded' does not exist" is being displayed

I am currently working on a React application that utilizes Typescript to fetch data from a Spring Data REST API (JPA repositories). When I make a specific request like "GET http://localhost:8080/notifications/1" with an ID, my JSON response does not pose ...

Combine array elements in Angular/Javascript based on a certain condition

Is there a way to combine elements from two arrays while avoiding duplicates? array = [ {id: 1, name:'abc'},{id: 1, name:'xyz'},{id: 2, name:'text1'},{id: 2, name:'text2'} ]; The desired output is: result = [{id: ...

Tips for leveraging angular CLI's async import feature with webpack

I've been attempting to utilize webpack's (2.2.1) async module loading as outlined in the documentation. In addition, I have explored various examples for reference. However, I keep encountering the error message Declaration or statement expecte ...

Verify if the keys are present within the object and also confirm if they contain a value

How can we verify keys and compare them to the data object? If one or more keys from the keys array do not exist in the object data, or if a key exists but its value is empty, null, or undefined, then return false; otherwise, return true. For example, if ...

How can I generate codegen types using typeDefs?

I have been exploring the capabilities of graphql-codegen to automatically generate TypeScript types from my GraphQL type definitions. However, I encountered an issue where I received the following error message: Failed to load schema from code file " ...

Naming convention for TypeScript accessors

Expanding on the previous solution When I convert the example object to JSON from the answer above: JSON.stringify(obj) The output is: {"_id":"3457"} If I intend to transmit this data over a service and store it in a database, I prefer not to use the ...

TypeScript is unable to identify import paths

Currently facing an issue with a specific error in my development environment. I am working with React, TypeScript, and node.js using VSCode on a Mac. I can't pinpoint exactly when this error started occurring. Despite the existence of the file route ...

How to dynamically incorporate methods into a TypeScript class

I'm currently attempting to dynamically inject a method into an external class in TypeScript, but I'm encountering the following error. Error TS2339: Property 'modifyLogger' does not exist on type 'extclass'. Here's the ...

Issues arise with the escape key functionality when attempting to close an Angular modal

I have a component called Escrituracao that handles a client's billing information. It utilizes a mat-table to display all the necessary data. When creating a new bill, a modal window, known as CadastrarLancamentoComponent, is opened: openModalLancame ...