Convert an array of objects into a single object with keys from each object in TypeScript

interface Item {
  slug: string;
  description: string;
}

interface Params {
  id: number;
  items: Item[];
}

function test(params: Params) {
  const result = {};
  
  for (const item of params.items) {
    result[item.slug] = "Hello";
  }
  
  return result;
}


const data = {
  id: 3,
  items: [
    { slug: "one", description: "This is one" },
    { slug: "two", description: "This is two" },
    { slug: "three", description: "This is three" },
  ]
}

const final = test(data)

Alright, I have some data that is passed to the test function. The test function generates an object with keys based on the value of slugs in the items along with a string value. My goal is to define the type of the final variable as follows:

{
  one: string;
  two: string;
  three: string;
}

Rather than getting this type:

Record<string, string>

In other words, I want it to extract slug values from items and create an object accordingly, explicitly specifying the type. Can someone please advise on how I can precisely define the return type of the test function to achieve this? Thank you.

Answer №1

To begin, you must adjust how you initialize the data. When TypeScript examines the object literal with a property like {slug: "one"}, it infers the type as {slug: string}, without recognizing your later intent to identify the literal type "one". Therefore, you lose the crucial information even before calling test(data).

The simplest solution is to employ a const assertion, which automatically infers literal types for literal values and uses readonly tuples for array literals:

const data = {
  id: 3,
  items: [
    { slug: "one", description: "This is one" },
    { slug: "two", description: "This is two" },
    { slug: "three", description: "This is three" },
  ]
} as const; 

Now we can turn our attention to test() and the types it interacts with. If you desire to maintain the actual literal types of the slug properties passed into test(), thereby making the output type of test dependent on its input type, you need to redefine test()'s call signature as generic. This implies that Params and Item should also be generic, possibly designed like so:

interface Item<K extends string = string> { 
  slug: K;
  description: string;
}

interface Params<K extends string = string> { 
  id: number;
  items: readonly Item<K>[];
}

These types use K as a generic parameter for the slug property type. I have assigned K a [default type argument](Note that https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#generic-parameter-defaults) of simply string, allowing you to refer to the Item and Params types without a specific type argument, maintaining consistency.

Additionally, I've converted items into a readonly array due to const assertions producing readonly arrays, eliminating unnecessary complications. Generally, if an array is not meant to be modified after acceptance, adding readonly to the type suffices. Although all arrays in TypeScript are assignable to their readonly equivalents, the reverse is not true, underscoring the nuances of using readonly in this context.


Finally, by making test generic and accepting a Params<K>, we achieve:

function test(params: Params<K>) {
  const result = {} as {[P in K]: string} // <-- need assertion

  for (const item of params.items) {
    result[item.slug] = "Hello";
  }

  return result;
}

I've explicitly asserted that result adheres to the type {[P in K]: string}, a mapped type akin to Record<K, string> utilizing the Record utility type. Essentially, this type denotes "an object type containing keys of type K and values of type string". An assertion becomes necessary since {} does not conform to this structure at first, necessitating alignment within the function's logic flow. Consequently, the return type of test mirrors the dependency on K:

const final = test(data)
/* const final: {
    one: string;
    two: string;
    three: string;
} */    

This demonstrates that test(data) identifies K as "one" | "two" | "three", hence returning the desired type

{one: string; two: string; three: string}
.

Access the Playground link here

Answer №2

If you want to ensure that your data remains constant, you can use the as const modifier to mark it as such. This will ensure that the type for slug is not just any string, but specific string literals:


interface Item {
  slug: string;
  description: string;
}

interface Params {
  id: number;
  items: Item[];
}

const data = {
  id: 3,
  items: [
    { slug: "one", description: "This is one" },
    { slug: "two", description: "This is two" },
    { slug: "three", description: "This is three" },
  ]
} as const

type Data = typeof data

type Slug = Data["items"][number]["slug"]

type Mapping = Record<Slug,string>;

View playground example here

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 best way to implement nested routing in React using the "exact" parameter for my specific use case?

Having some trouble accessing other components from the Router Home page. The current structure I'm using is causing the "404" page to not open. Additionally, when I add the exact path="/" parameter to the PrivateRoute, the Home component fails to ren ...

"Execution of the console.log statement occurs following the completion of the request handling

When I have a piece of middleware that responds if no token is found, why does the console.log line still run after the request is responded to? I always believed that the res.json call would "end" the middleware. Any insights on this behavior would be g ...

Handling errors within classes in JavaScript/TypeScript

Imagine having an interface structured as follows: class Something { constructor(things) { if (things) { doSomething(); } else return { errorCode: 1 } } } Does this code appear to be correct? When using TypeScript, I en ...

Baffled by the intricacies of jQuery

As a newcomer to jQuery, I stumbled upon this code snippet and am curious about its function. It seems like it is replacing an element with the id reveal to a link with the class html5lightbox and id reveal. However, the part that puzzles me is $( '#r ...

Updating device information in real-time using React Native

Currently, I am utilizing react-native-device-info to access the DeviceLocale or DeviceCountry. However, I am wondering if there is a method to update Device-info without requiring a complete restart of the app. For instance, when my device language is se ...

What is the best way to simulate Graphql queries and disable the loader in order to test a React component when loading is set to false using Jest and Enzyme?

Currently, I am using code generation on the frontend to generate types and employing GraphQL queries to fetch data in this particular format - const { data, loading, error } = useSampleQuery({ variables: { a: 1, b: 2 } }) When it comes ...

AngularJS encountered an error following a successful GET request

I'm attempting to retrieve data from the server using this code snippet. $scope.get_file_list = function() { delete $http.defaults.headers.common['X-Requested-With']; //We don't want OPTIONS but GET request $htt ...

Extract the byte data from an audio file and then proceed to play the file by utilizing AudioTrack

Is it possible to read an audio file from an SD card and convert it into a byte array to play through AudioTrack? Would the process involve playing the file through AudioTrack like this? track.play(); track.write(byte_Arr,0,b ...

When I tried using the HTML 5 boilerplate extension in VS code, I received an error message indicating that it was not functioning properly

I've recently entered the realm of tech and decided to give VS Code a try. After downloading it, I installed several extensions, but unfortunately some of them aren't functioning properly. My primary issue is with the HTML5 boilerplate extension. ...

What could be causing the issue with resizing windows when using a modal?

My modal contains a slider, but it doesn't show up on the screen when the modal is displayed. Interestingly, if I manually resize the window, the slider appears. I looked into this issue and found that others have mentioned it's related to the mo ...

A fusion of two arrays and their concatenation

There are two arrays linked to a scope through http get: $scope.allShops this array holds all the shop details and $scope.allCds holds all the CDs. While both arrays work fine individually with Ng-Repeat providing the necessary output, I want to creat ...

Tips for selecting the key as an option value in VueJS when using v-for loop

Currently, I'm utilizing Vue 3 along with Bootstrap 5. In my project, there is a select element containing various options. Right now, the displayed text (option.TEXT) is passed as the value to my methods when any changes are made. However, what I ac ...

How to selectively make properties optional in Typescript conditions

Currently, I am working on creating a utility type to unwrap nested monads of Options in my code. Here is the progress I have made so far: export interface Option<T> { type: symbol; isSome(): boolean; isNone(): boolean; match<U>(fn: Mat ...

JavaScript may fail to execute properly if the <!doctype html> declaration is not inserted in the correct syntax

While building a web page, I encountered an issue with my JavaScript code not working when using <!doctype html> or any type of <!doctype> at the top of the page. Can someone explain this error? After researching online, I learned that <!do ...

Guide to integrating ua-parser-js with angular 4

Is there a way to retrieve the OS name, OS version, browser name, browser version, and device from the user agent using User Agent Parser JS in Angular 4? I attempted using [email protected], but it did not provide me with the OS version and device ...

Add navigation dots to the slider in order to align them with the specified div element

Visit this JSFiddle link for the code <html> <link href="./style.css" rel="stylesheet" type="text/css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script&g ...

How can we add styles to text entered into a form input field in a targeted way?

Is there a way to apply styling to the second word entered into a form input text field after the user hits "enter" and the data is output? In this scenario, the text always follows the format of 3 numbers, followed by a space, and then a name. For examp ...

Experimenting with the inner workings of a method by utilizing vue-test-utils alongside Jest

Is there a way to properly test the internal logic of this method? For instance: async method () { this.isLoading = true; await this.GET_OFFERS(); this.isLoading = false; this.router.push("/somewhere"); } This method toggles isLoading, ...

Encountering a Typings Install Error When Setting Up angular2-localstorage in WebStorm

I am currently using Windows and WebStorm as my development environment. I attempted to install the angular2-localstorage package by running the command npm install angular2-localstorage, but encountered an error. After realizing that the angular2-localst ...

Having Trouble with Axios PUT Request in React and Redux

I'm having trouble making a PUT request to the server. I understand that for a PUT request, you need an identifier (e.g id) for the resource and the payload to update with. This is where I'm running into difficulties. Within my form, I have thes ...