Utilizing the index of a generic type within TypeScript

I am attempting to design generic message types that become increasingly specific, with the most precise type being inferred during the function call or class creation.

Consider the following code snippet:

type MessageHeaderType = 'REQUEST'|'RESPONSE'|'UPDATE'|'ERROR'|'EVENT'

type UnknownRecord = object

interface Message<MessageBody = UnknownRecord> {

  header: {
    id?: string
    type: MessageHeaderType
  },
  body: MessageBody

}

interface MessageUpdate<MessageBody = MessageUpdateBody> extends Message<MessageBody> {

  header: {
    id: string
    type: 'UPDATE'
  }
}

interface MessageUpdateBody {

  code: string
  data?: UnknownRecord

}

This setup works smoothly and allows for easy creation of a MessageUpdate:

const ExampleMessageUpdate: MessageUpdate = {

  header: {
    id: '123',
    type: 'UPDATE'
  },
  body: {
    code: 'SOME_UPDATE',
    data: {
      foo: 'bar'
    }
  }
} // <== All good!

The issue arises when giving users the ability to generate their custom message update body "on the fly" (still at compile-time, obviously!). The code below does not permit indexing the generically passed implementation in a specific MessageUpdate type:

function createMessage<ThisMessageUpdateBody>(code: string, data?: ThisMessageUpdateBody['data']): MessageUpdate<ThisMessageUpdateBody> {

  const message: MessageUpdate<ThisMessageUpdateBody> = {
    header: {
      id: '123',
      type: 'UPDATE'
    },
    body: {
      code,
      data
    }
  }

  return message
  
}

An error is triggered regarding the function parameters:

Type '"data"' cannot be used to index type 'ThisMessageUpdateBody'

Is there a way to further narrow down this type without encountering an error?

Answer №1

Update

Here's a slightly modified version to adjust to OP's comment.

If utilizing an object as the sole parameter of the function is not feasible, I would suggest using type aliases for safety and steering clear of string indexes.

type BodyCodeType = string;
type BodyDataType = UnknownRecord;

interface MessageUpdateBody {
  code: BodyCodeType;
  data?: BodyDataType;
}

interface MessageUpdate<MessageBody = MessageUpdateBody>
  extends Message<MessageBody> {
  header: {
    id: string;
    type: 'UPDATE';
  };
}

function createMessage<ThisMessageUpdateBody extends MessageUpdateBody>(
  code: BodyCodeType,
  data: BodyDataType
): MessageUpdate<ThisMessageUpdateBody> {
  const message: MessageUpdate<ThisMessageUpdateBody> = {
    header: {
      id: '123',
      type: 'UPDATE',
    },
    body: {
      code,
      data,
    } as ThisMessageUpdateBody,
  };

  return message;
}

const customMsg = createMessage('test_code', { foo: 'bar' });

console.log(customMsg); // OK

By the way, remember to include the as ThisMessageUpdateBody cast to prevent confusion during compilation on the body:

https://i.sstatic.net/H9nJH.png


Personally, I prefer relying on data inference from the template type and therefore opt for utilizing an object for the function parameter. For example:

function createMessage<ThisMessageUpdateBody extends MessageUpdateBody>({
  code,
  data,
}: ThisMessageUpdateBody): MessageUpdate<ThisMessageUpdateBody> {
  const message: MessageUpdate<ThisMessageUpdateBody> = {
    header: {
      id: '123',
      type: 'UPDATE',
    },
    body: {
      code,
      data,
    } as ThisMessageUpdateBody,
  };

  return message;
}

const customMsg = createMessage({
  code: 'test_code',
  data: { foo: 'bar' },
});

Avoiding strings for indexing also provides benefits since changing from data to payload within MessageUpdateBody will result in a clear compile-time error.

Answer №2

To retrieve the index of a generic type, one approach is to define another generic and set this generic as the type. For instance, in your scenario, it could look something like this:

function createMessage<ThisMessageUpdateBody extends MessageUpdateBody, Data extends ThisMessageUpdateBody['data']>(code: string, data?: Data): MessageUpdate<ThisMessageUpdateBody> {
    const body = {
        code,
        data
    };
  const message = {
    header: {
      id: '123',
      type: UPDATE as typeof UPDATE
    },
    body
  }

  return message
  
}

However, the above code snippet still triggers an error because ThisMessageUpdateBody could be any subtype of MessageUpdateBody and may contain properties other than just code & data (as shown in the error below).

Type '{ code: string; data: Data | undefined; }' is not assignable to type 'ThisMessageUpdateBody'.
      '{ code: string; data: Data | undefined; }' is assignable to the constraint of type 'ThisMessageUpdateBody', but 'ThisMessageUpdateBody' could be instantiated with a different subtype of constraint 'MessageUpdateBody'.(2322)

Suggestion 1:

Instead of using parameters code and data, modify createMessage to take a single parameter named body.

function createMessage<ThisMessageUpdateBody extends MessageUpdateBody= MessageUpdateBody>(body: ThisMessageUpdateBody): MessageUpdate<ThisMessageUpdateBody> {
  const message = {
    header: {
      id: '123',
      type: UPDATE as typeof UPDATE
    },
    body
  }

  return message
  
}

Suggestion 2:

Rather than defining the generic type for messageBody in createMessage, declare the generic type for data in the body. This ensures that the message body always has two specific properties (code: string & data: CustomData)

// Declare generic for "body.data" type within MessageBody
interface MessageUpdateBody<MessageBodyData = UnknownRecord> {

  code: string
  data?: MessageBodyData

}

function createMessage<MessageBodyData = UnknownRecord>(code: string, data: MessageBodyData): MessageUpdate<MessageUpdateBody<MessageBodyData>> {
  const message = {
    header: {
      id: '123',
      type: UPDATE as typeof UPDATE
    },
    body: {
        code,
        data
    }
  }

  return message
  
}

Link to TS snippet 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

Utilizing object as props in ReactJS with TypeScript

I'm new to working with ReactJS, and I made the decision to use typescript for my current project. The project is an application that fetches movies from an API and displays them. Take a look at the app: import React from 'react'; import &a ...

Encountering an error in Express while attempting to upload an image due to the inability to read the property 'file' of undefined

I am currently learning Express framework. I encountered an error while trying to upload an image using Express. The error message I received is "Cannot read property 'file' of undefined." Below are the code snippets that I am using, and I&apo ...

Multiple occurrences of the cookie found in the document.cookie

My client is using IE9 and the server is asp.net (specifically a SharePoint application page). Within the Page_Load method of a page, I implemented the following code: Response.Cookies["XXXXX"].Value = tabtitles.IndexOf(Request.Params["tab"]).ToString(); ...

Is there a way to include multiple TinyMCE editors with unique configurations for each one?

Is it possible to incorporate multiple TinyMCE editors on a single page, each with its own distinct configuration settings? If so, how can this be achieved? ...

What is the best approach for creating routes with parameters of varying lengths?

Due to the structure of the website's url, the parameters will vary, making it impossible to set a fixed number of parameters. How can we modify the app-routing.module.ts file to accommodate this? url => /products/cat1/cat2/cat3/cat4 ... const rou ...

Is Ajax capable of processing and returning JSON data effectively?

I am in the process of making modifications to my codeigniter application. One of the changes I am implementing is allowing admin users to place orders through the admin panel, specifically for received orders via magazines, exhibitions, etc. To achieve ...

Tips for accessing jQuery UI tab elements and adjusting visibility for specific panels

How can I retrieve the panel numbers of jQuery UI tabs and adjust their visibility using CSS? I am looking to identify the panel numbers of each tab and control the visibility of certain tabs. <div id="tabs"> <ul> <li><a href="#"> ...

Resolving NestJS Custom Startup Dependencies

In my setup, I have a factory responsible for resolving redis connections: import {RedisClient} from "redis"; export const RedisProvider = { provide: 'RedisToken', useFactory: async () => { return new Promise((resolve, reject ...

Nestjs crashes with "terminated" on an Amazon EC2 instance

https://i.stack.imgur.com/3GSft.jpgMy application abruptly terminates with the error message "killed" without providing any additional information or stack trace. Interestingly, it functions properly when run locally. Any assistance would be greatly appr ...

Executing a single insert statement in a TypeScript Express application using PostgreSQL is taking over 240 milliseconds to complete

Seeking assistance with optimizing a db insert operation test using mocha for a node.js express app that utilizes fp-ts and pg npm package. The tests run successfully, but the insert test is taking over 240 ms to complete. The database table has a maximum ...

Is it considered inefficient to import every single one of my web components using separate <script> tags?

Currently, I am in the process of developing a website with Express + EJS where server-side rendering is crucial. To achieve this, I am utilizing web components without shadow dom. Each page type (home, post, page) has its own EJS view. For instance, when ...

How can I apply styling to Angular 2 component selector tags?

As I explore various Angular 2 frameworks, particularly Angular Material 2 and Ionic 2, I've noticed a difference in their component stylings. Some components have CSS directly applied to the tags, while others use classes for styling. For instance, w ...

Testing for expressjs 4 using Mocha revealed unexpected behavior when attempting to spy on a function called within a promise. Despite setting up the

I have encountered a situation with some test cases structured like this: it('does stuff', function(done) { somePromise.then(function () { expect(someSpy).to.have.been.called done() }) }) When the assertion in a test case fails, it ...

Slider buttons are not appearing in Time Picker

I recently added a Time Picker to my project, but I'm experiencing an issue where the slider buttons for selecting time are not showing up. Could you please refer to this image: Checking the console, there don't seem to be any errors present. ...

Leveraging functionality from an imported module - NestJS

Currently, I am utilizing a service from a services module within the main scaffolded app controller in NestJS. Although it is functioning as expected - with helloWorldsService.message displaying the appropriate greeting in the @Get method - I can't ...

What is the best way to insert a newline in a shell_exec command in PHP

I need assistance with executing a node.js file using PHP. My goal is to achieve the following in PHP: C:proj> node main.js text="This is some text. >> some more text in next line" This is my PHP script: shell_exec('node C:\pr ...

Styling on Material UI Button disappears upon refreshing the page

I've been using the useStyles function to style my login page, and everything looks great except for one issue. After refreshing the page, all styles remain intact except for the button element, which loses its styling. Login.js: import { useEffect, ...

Proper method for displaying modifications in QueryList from @ContentChildren

I'm having trouble with rendering components and here is the code snippet: <my-component> <ng-template *ngFor="let item of data"> <child-component> <div> {{ data.title }} </div> </child-c ...

Angular's onreadystatechange event is triggered when the state

Hey there, I'm new to Angular and had a question. Is it possible to use the $http service in Angular to trigger a function whenever there is any change in the ready state/status, not just for success or failure? If so, what would be the equivalent ang ...

Exploring the process of retrieving token expiration in TypeScript using the 'jsonwebtoken' library

Currently utilizing jsonwebtoken for token decoding purposes and attempting to retrieve the expiration date. Encountering TypeScript errors related to the exp property, unsure of the solution: import jwt from 'jsonwebtoken' const tokenBase64 = ...