Can you inherit a type based on the keyof template in TypeScript?

I attempted a small hack to explore how DOM ts files map different element names to their types.

My experiment involved trying to have a type MyType extend a different set of fields depending on the value of a string. Here is what I tried:

interface MessagesToContentMap {
   "error": {message: string},
   "success": {result: number}
}

My goal was to inherit this in a general message, which goes against the usual practice:

interface GenericMessage<KType extends keyof MessagesToContentMap> extends MessagesToContentMap[KType] {
    type: KType
}

The idea behind this was that I hoped JavaScript would automatically infer the message type in if statements like this:

const msg: GenericMessage<any> = {};
if(msg.type === "error") {
    // The type system should recognize this as {message: string, type: "error"}
}

Although this works with typeof, my specific code did not work as expected. It only hinted at the type property and not the other fields. However, it correctly suggested "error" and "success" as options for the type.

I also tried another approach, which functioned but did not align with the actual messages I would receive:

interface GenericMessage<KType extends keyof MessagesToContentMap> {
    type: KType;
    data: WorkersInternal.MessagesToContentMap[KType];
}

Even with this solution, an explicit cast was still needed. For example,

(testMessage as GenericMessage<"wrk">).data
.

Answer №1

To achieve the desired functionality, consider implementing a discriminated union instead of a generic type.

type Details = { kind: "error"; message: string; } | { kind: "success"; result: number; }

This approach will provide the specific narrowing behavior you are seeking:

const info: Details;
if (info.kind === "error") {
    info.message.toUpperCase();
} else {
    info.result.toFixed();
}

The next step involves dynamically deriving the structure of Details from the definition of DataToContentMapping, eliminating manual declaration. With this in mind:

interface DataToContentMapping {
    "error": { message: string },
    "success": { result: number }
}

You can construct Details as a distributive object type through intersection types like so:

type Details = { [T in keyof DataToContentMapping]:
    { kind: T } & DataToContentMapping[T]
}[keyof DataToContentMapping]

The expression

{ kind: T } & DataToContentMapping[T]
effectively defines the desired generic aspect. Interface extension cannot be used since DataToContentMapping[T] lacks statically known keys, but intersections offer a solution. The resultant objects maintain type integrity despite being combined using intersection.

By applying

{[T in keyof DataToContentMapping]: ⋯}[keyof DataToContentMapping]
, a distributive object type is created. This technique distributes the middle portion across each member T of keyof DataToContentMapping], yielding a union. The process involves mapping properties to new object types and indexing into that set with all available keys, culminating in a union output.

Confirm that the conditional behavior depicted earlier remains consistent with the revised definition of Details. Any adjustments made to DataToContentMapping will automatically propagate to the Details type.


Explore the code snippet on TypeScript Playground

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

Is there a way to programmatically control a Bootstrap Dropdown in Angular?

Need help with opening and closing a Dropdown in my View using my typescript class. Any suggestions on how to achieve this? Appreciate any assistance! <div ngbDropdown class="d-inline-block"> <button class="btn" id=&quo ...

Interactive 3D model movable within designated area | R3F

I am currently facing a challenge in limiting the drag area of my 3D models to the floor within my FinalRoom.glb model. After converting it to tsx using gltfjsx, I obtained the following code: import * as THREE from "three"; import React, { useRe ...

Event typeORM on afterUpdate in NestJS

After every update of my data in the log table, I want to insert an entry into another table. To achieve this, I have created an EntitySubscriberInterface. The event is triggering correctly, but the entity array does not include the updated id. async afte ...

Ditch the if-else ladder approach and instead, opt for implementing a strategic design

I am currently working on implementing a strategic design pattern. Here is a simple if-else ladder that I have: if(dataKeyinresponse === 'year') { bsd = new Date(moment(new Date(item['key'])).startOf('year&apos ...

Enabling cookie communication between NestJS server and Next.js frontend

I seem to be encountering challenges when trying to set cookies from a NestJS backend into my Next.js app. My NestJS application is running on port 3001 and here is my bootstrap setup: async function bootstrap() { const app = await NestFactory.create(Ap ...

The type 'never' does not have a property named 'map'

Whenever I try to make an axios get request within a next.js getServerSideProps function, I encounter a persistent TypeScript error underline on the map method. Despite troubleshooting extensively, I have not been able to resolve it. The request successf ...

Implementing a personalized pipe to enhance search functionality in a data table

I am currently working on a project that involves displaying data in a table. I want to add a search bar functionality that allows users to filter the table data. I have attempted to use a pipe to achieve this, but I am facing challenges and unsure of the ...

Disabling an anchor using the 'disabled' property is proving to be a challenge for me

I'm attempting to dynamically enable or disable an anchor element based on the user's role. So far, I've tried a few different methods: document.getElementById('myBtn').disabled = true; However, this returns an error: The propert ...

Version 4.0 of d3 introduces an import statement that provides a __moduleExports wrapper

Having difficulty with an import statement in my D3 4.0 and Ionic2/Angular2 project. I believe the import statement is correct, as everything compiles successfully. import * as d3Request from 'd3-request'; export class HomePage { construc ...

What is the process for designing a mapping type that involves two enums?

I need a solution to connect these two types and create mappings: type MappedEnum<T extends string, A extends string, B extends string> = { [key in T]: {[key in A]: B}; [key in T]: {[key in B]: A}; }; enum Greek { ALPHA = 'A', BET ...

Angular release 6: A guide on truncating text by words rather than characters

I'm currently enhancing a truncate pipe in Angular that is supposed to cut off text after 35 words, but instead it's trimming down to 35 characters... Here is the HTML code: <p *ngIf="item.description.length > 0"><span class="body-1 ...

Mastering the Art of Concise Writing: Tips to

Is there a way to write more concisely, maybe even in a single line? this.xxx = smt.filter(item => item.Id === this.smtStatus.ONE); this.yyy = smt.filter(item => item.Id === this.smtStatus.TWO); this.zzz = smt.filter(item => item.Id == ...

The namespace does not contain any exported member

Every time I attempt to code this in TypeScript, an error pops up stating The namespace Bar does not have a member named Qux exported. What could possibly be causing this and how can I resolve it? class Foo {} namespace Bar { export const Qux = Foo ...

Developing a hover-triggered tooltip feature in a React application

A tooltip has been created that appears when hovering over an element, displaying the full name of the product called productName. <div className="product-select-info" onMouseEnter={e => productNameHandleHover(e)} onMouseLeave={productNameHand ...

What is the method for implementing type notation with `React.useState`?

Currently working with React 16.8.3 and hooks, I am trying to implement React.useState type Mode = 'confirm' | 'deny' type Option = Number | null const [mode, setMode] = React.useState('confirm') const [option, setOption] ...

Can you explain the usage of the syntax in Angular marked with the @ sign, such as @NgModule, @Component, and @Injectable?

Angular utilizes specific syntax for declaring modules, components, and services, as shown in the example below: @Component({ ... }) export class AppComponent However, this syntax is not commonly seen in traditional JavaScript development. It begs the ...

Creating a wrapper component to enhance an existing component in Vue - A step-by-step guide

Currently, I am utilizing quasar in one of my projects. The dialog component I am using is becoming redundant in multiple instances, so I am planning to create a dialog wrapper component named my-dialog. my-dialog.vue <template> <q-dialog v-bin ...

What is the process for assigning a predefined type that has already been declared in the @types/node package?

Is there a way to replace the any type with NetworkInterfaceInfo[] type in this code snippet? Unfortunately, I am unable to import @types/node because of an issue mentioned here: How to fix "@types/node/index.d.ts is not a module"? Here is the o ...

Update an API call to switch from promises to observables, implementing axios

Recently, I have been experimenting with using rxjs for API requests in a React application and this is the approach that I have come up with. What are your thoughts on this method? Are there any best practices that you would recommend following? If you ...

ERROR TypeError: Unable to access the 'nativeElement' property since it is undefined in Angular 5

I have encountered a problem while working on my application. Although similar issues have been asked before, I believe mine is slightly different. In my application, when a user deletes products from their cart, I want to automatically close the modal wi ...