Combining subclasses in TypeScript

Do you need help with a tricky situation? 😅

The Case:

Imagine a scenario where there's a main class involving multiple sub-classes:

// Main class
class Something <T> {
    constructor (x: T) {
        // ...
    }
    doSomething (value: T) {
        // ...
    }
}

// Subclasses
class AnotherThing extends Something<string> {}
class YetAnotherThing extends Something<number> {}

// Union of subclasses
type SomethingUnion = AnotherThing | YetAnotherThing;

Now let's say we have an object type that contains values of type SomethingUnion...

type SomethingMap = {
    [key: string]: SomethingUnion;
}

...and a mapped type that extracts the type parameter from each element within a given SomethingMap (known as DataOf):

// Extracts `T` from `Something<T>`.
type GetT<S extends Something<any>> = S extends Something<infer U> ? U : never

// Mapped type to extract type parameter from every subclass of `Something`.
type DataOf<T extends SomethingMap> = {
    [K in keyof T]: GetT<T[K]>;
}

The Problem:

If you have a value someMap (of type SomethingMap) and another value someOtherMap, which is of type DataOf created from someMap, and you wish to iterate over the entries of someMap, TypeScript may infer the type of thing.doSomething(...)'s parameter to be never. How can we make it match the type of valueToDoThingsWith instead?


// Assume someMap was defined earlier as type `SomethingMap`
// and someOtherMap was established as having a type similar to `DataOf<typeof someMap>`

const entries = Object.entries(someMap); // [string, SomethingUnion][]


const mappedEntries = entries.map(([key, thing]) => {
    const valueToDoThingsWith = someOtherMap[key] // string | number

    // Here, `thing` has type `SomethingUnion`, but the parameter `value` resolves to `never`,
    // conflicting with `valueToDoThingsWith` of type "string | number".
    thing.doSomething(valueToDoThingsWith)

    // ...
})

Is there a solution to this dilemma, ensuring that doSomething accepts a parameter matching valueToDoThingsWith's type?

Answer â„–1

To create a versatile helper function, consider developing a generic function that takes in two maps. The first map, named someOtherMap, should contain objects of type T. The second map, called someMap, should consist of mapped types where each key is an element from T, and its value is defined as

{[K in keyof T]: Something<T[K]>}
. This structure ensures that for every key key of type K, the compiler recognizes that the value stored at someOtherMap[key] is suitable to be used as input for the method someMap[key].doSomething():

function process<T extends object>(
    someMap: { [K in keyof T]: Something<T[K]> },
    someOtherMap: T
) {

    const entries = Object.entries(someMap) as // <-- asserting
        { [K in keyof T]: [K, Something<T[K]>] }[keyof T][];

    const mappedEntries = entries.map(
        <K extends keyof T>([key, thing]: [K, Something<T[K]>]) => {
            const valueToDoThingsWith = someOtherMap[key]
            thing.doSomething(valueToDoThingsWith)
        })
}

The key aspect here is using a type assertion to ensure that the output of Object.entries() aligns with the expected array format containing key-value pairs where values are of compatible types. By making both the entries.map() callback and thing[key] operations generic, we can achieve full versatility without explicitly referring to specific entities like SomethingUnion or data types like string or number.


Let's verify that process() functions correctly:

const map = { a: new AnotherThing("abc"), b: new YetAnotherThing(123) };
const otherMap = { a: "def", b: 456 }
process(map, otherMap); // okay

This demonstration confirms successful execution only if TypeScript can validate the relationship between map and someOtherMap. However, discrepancies may arise if data comes from external sources like API responses, warranting additional handling beyond the scope of this implementation.

Playground link to code

Answer â„–2

These are two possible solutions:

Solution 1:

Approach:

Instead of creating a union of the subclasses themselves, use GetT<...> to generate a union of the types of T in each Something<T> subclass.

// Generate a union of `GetT<...>` results for each constituent of SomethingUnion
type TypesOfT = GetT<SomethingUnion>;

Subsequently, utilize this TypesOfT type as the type argument to Something<...>:

type BetterSomethingUnion = Something<TypesOfT>;

Explanation:

Consider the following map call:


// Note: `someMap` and `someOtherMap` have the types discussed above.

const entries = Object.entries(someMap); // [string, SomethingUnion][]


const mappedEntries = entries.map(([key, thing]) => {
    const valueToDoThingsWith = someOtherMap[key] // string | number

    // `thing` is of type `SomethingUnion`
    thing.doSomething(valueToDoThingsWith) // error

    // ...
})

In the callback function, thing has the type SomethingUnion, which is equivalent to:

type SomethingUnion = AnotherThing | YetAnotherThing;
type SomethingUnion = Something<string> | Something<number>;

However, for the desired outcome, a type like Something<string | number> is necessary, so that the argument to .doSomething(...) is typed as string | number, as intended.

TypeScript cannot equate

Something<string> | Something<number>
with Something<string | number>, leading to an evaluation of never for the argument in thing.doSomething(...).

Employing the BetterSomethingUnion type, as outlined in the solution, will produce the following:

type TypesOfT = GetT<SomethingUnion>; // string | number
type BetterSomethingUnion = Something<TypesOfT>; // Something<string | number> !!!

This meets the requirements perfectly. Therefore, a type such as the one below could replace SomethingMap:

type BetterSomethingMap = {
    [key: string]: BetterSomethingUnion;
};

Option 2 (not advisable)

Approach:

Simply cast valueToDoThingsWith to never (although this may lead to problems down the line if not handled correctly):

// Regular TypeScript (`.ts`) file only:
thing.doSomething(<never>valueToDoThingsWith);

// OR

// Regular TypeScript (`*.ts`) files, and TypeScript JSX (`*.tsx`) files:
thing.doSomething(valueToDoThingsWith as never);

Explanation:

By casting valueToDoThingsWith to never, TypeScript allows valueToDoThingsWith to be treated as assignable to the parameter of thing.doSomething(...), where never is expected as the parameter type, triggering typechecking errors.

Reasons for Not Recommending:

While casting to never resolves the issue at hand, it may overlook other potential issues arising from using SomethingMap over BetterSomethingMap (or an equivalent type). This approach can also complicate code maintenance, as it does not address the root cause of the error effectively.

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

What is the process for extracting TypeScript types from GraphQL query and mutation fields in order to get args?

I am experiencing difficulties with utilizing TypeScript and GraphQL. I am struggling to ensure that everything is properly typed. How can I achieve typed args and parent properties in Root query and mutation fields? For instance: Server: export interfa ...

Retrieving User's Theme Preference from Local Storage in Next.js Instantly

As mentioned in various other responses, such as this one, Next.js operates on both the client and server side, requiring a guard to properly fetch from localStorage: if (typeof localStorage !== "undefined") { return localStorage.getItem("theme") } else ...

Utilize JSX attributes across various HTML elements

I'm looking for a solution to efficiently add JSX attributes to multiple elements. Here are the example attributes I want to include: class?: string; id?: string; style?: string; And here are the example elements: namespace JSX { interface Int ...

Bringing in the RangeValue type from Ant Design package

Currently working on updating the DatePicker component in Ant Design to use date-fns instead of Moment.js based on the provided documentation, which appears to be functioning correctly. The suggested steps include: import dateFnsGenerateConfig from ' ...

In Angular 17, is there a way to trigger a component's method when a Signal is modified?

Our component is designed to monitor signals from a Service: export class PaginationComponent { private readonly pageSize = this.listService.pageSize.asReadonly(); private readonly totalCount = this.listService.totalCount.asReadonly(); readonly pag ...

There is an issue with the Angular Delete request functionality, however, Postman appears to be

HttpService delete<T>(url: string): Observable<T> { return this.httpClient.delete<T>(`${url}`); } SettingsService deleteTeamMember(companyId: number, userId: number): Observable<void> { return this.httpService.delete< ...

What is the reason for the retrieval of jquery-3.5.1.min.js through the request.params.id expression?

For my school project, I am using Express.js with TypeScript to create a simple app. This router is used for the edit page of a contact list we are developing. It displays the ID of the current contact being edited in the search bar. The problem arises whe ...

Having Trouble with Typescript Modules? Module Not Found Error Arising Due to Source Location Mismatch?

I have recently developed and released a Typescript package, serving as an SDK for my API. This was a new endeavor for me, and I heavily relied on third-party tools to assist in this process. However, upon installation from NPM, the package does not functi ...

An error occurred while attempting to set up Next-auth in the process of developing

In my Next.js app, I have implemented next-auth for authentication. During local development, everything works fine with 'npm install' and 'npm run dev', but when I try to build the project, I encounter this error message: ./node_modul ...

Error encountered with tsc-generated .d.ts files stating "Namespace 'Jimp' not found"

Currently, I am in the process of developing an NPM package, and within my codebase lies a specific class that looks like this: import { MIME_PNG } from 'jimp'; import { IDimensions } from './spritesheet'; /** * Representing a single ...

Error: Unexpected character U found at the beginning of the JSON data when using JSON.parse in Angular 8

Lately, I came across an issue while making changes to some parts of my previous code. The error caught my attention as it occurred when trying to pass a specific object or a part of that object in Angular 8, NodeJS with express, and mongoose. Upon checki ...

Developing a dynamic object in Typescript to structure and optimize API responses

Currently Working Explanation: This is similar to the data array received from the API response responseBarDataStacked = [ { sku: "Data 1", month: "Jun", value: 20 }, { sku: "Data 2", month: "Jun", value: 25 ...

Utilizing a Link element in conjunction with ListItem and Typescript for enhanced functionality

I am currently using material-ui version 3.5.1 My goal is to have ListItem utilize the Link component in the following manner: <ListItem component={Link} to="/some/path"> <ListItemText primary="Text" /> </ListItem> However, when I tr ...

Learn the process of transferring dropdown one component's value to another component in Angular

I'm facing an issue with removing the value of a dropdown from the table component to the ooptymodel component. Even after using input and output decorators, the solution doesn't seem to work. Can someone guide me on how to successfully remove th ...

Executing React's useEffect hook twice

As I work on developing an API using express.js, I have implemented an authentication system utilizing JWT tokens for generating refresh and access tokens. During testing with Jest, Supertest, and Postman, everything appears to be functioning correctly. O ...

Why isn't the table in the select query updating after an insert query is executed in Express?

Seeking assistance! Currently, I am delving into express and typescript. I have encountered an issue where the table from a select query does not update after an insert query when rendering a view. Strangely, the data in the table remains unchanged (showin ...

Avoid the sudden change in page content when using Router.Navigate

When the link below is clicked, the current page jumps to the top before proceeding to the next page. <a href="javascript:void(0);" (click)="goToTicket(x.refNo, $event)">{{x.ticketTitle}}</a> component.ts goToTicket(refNo, e) { e.prev ...

Issue with Typescript Conditional Type not being functional in a function parameter

For a specific use-case, I am looking to conditionally add a key to an interface. In attempting to achieve this, I used the following code: key: a extends b ? keyValue : never However, this approach breaks when a is generic and also necessitates explicit ...

What is the specific reference of the first parameter in NLS localize?

Regarding the question on localizing VSCode extensions, I am also curious why the localize function requires two parameters. When it comes to localizing settings, it's a simple process of replacing literal values with tokens surrounded by percent sig ...

What are some ways to leverage a promise-returning callback function?

Here is a function that I have: export const paramsFactory = (params: paramsType) => { return ... } In a different component, the same function also contains await getPageInfo({ page: 1 }) after the return .... To make this work, I need to pass a cal ...