Typescript objects may contain keys that are dependent on certain parameters

I have a challenge with constructing an object that requires querying multiple database tables, resulting in a time-consuming process. To address this issue, clients of the object need to specify which specific parts they require. For example, let's consider a User object that may sometimes include Purchases and sometimes Friends. Currently, I am utilizing optional properties for this purpose:

interface User {
  id: number
  purchases?: Purchase[]
  friends?: Friend[]
  friendsOfFriends?: Friend[]
}

Clients requesting a User can use getUser(['purchases']) to retrieve a user where the purchases key is defined.

The downside is that whenever I utilize purchases, friends, or friendsOfFriends, I must inform Typescript about the existence of these types (e.g., user.purchases![0] or

user.purchases && user.purchases[0]
), which can be cumbersome.

Is there a way in Typescript to indicate that the parameters passed in determine which keys are present in the returned value? For instance:

  • getUser([]) results in an {id: number}
  • getUser(['purchases']) results in an
    {id: number; purchases: Purchase[]}
  • getUser(['network']) yields an
    {id: number; friends: Friend[]; friendsOfFriends: Friend[]}
    -- notice how network retrieves friends and friendsOfFriends
  • getUser(['purchases', 'network'])
    gives us an
    {id: number; purchases: Purchase[]; friends: Friend[]; friendsOfFriends: Friend[]}

In actual scenarios, there are more than two possible keys to include, and creating numerous overloaded types is not desirable.

Is it feasible to achieve this functionality in Typescript?

Appreciate any guidance!

Answer №1

I will let you handle the implementation of getUser(), as well as convincing the compiler that your implementation aligns with the type definition. Essentially, I am treating getUser() as if it exists in pure JavaScript land, and my role is to define its type so that the compiler can correctly process calls to it.

To start, the compiler needs a mechanism to understand the relationship between arguments passed to getUser() and which sets of keys from User they correspond to. Here's an example of how this mapping might look like:

interface KeyMap {
    purchases: "purchases",
    network: "friends" | "friendsOfFriends"
}

With this information, we need to instruct the compiler on how to calculate UserType<K> for a given set of keys K based on KeyMap. One possible way to achieve this is:

type UserType<K extends keyof KeyMap> =
    User & Pick<Required<User>, KeyMap[K]> extends 
    infer O ? { [P in keyof O]: O[P] } : never;

The crucial part here is

User & Pick<Required<User>, KeyMap[K]>
: This ensures that the result is always a User by including it in an intersection. Additionally, we extract the properties pointed out by K from Required<User> using Pick. The Required<T> type converts optional keys into required ones.

The section starting with extends infer O... essentially leverages conditional type inference to transform the complex type User & Pick... into a clear object type with explicit properties. If you prefer seeing

User & Pick<Required<User>, "purchases">
instead of the object types below, you can remove everything after extends.

Finally, the typing for the getUser() function appears as follows:

declare function getUser<K extends keyof KeyMap>(keys: K[]): UserType<K>;

This function takes an array of KeyMap keys and returns the corresponding UserType<K>.

...

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

Encountering a TypeScript error while trying to pass the myDecorator function as a decorate prop to React

Encountering a TS error that states: (property) decorate?: ((entry: NodeEntry<Node>) => BaseRange[]) | undefined Type '([node, path]: [node: any, path: any]) => { anchor: { path: any; offset: string | number; }; focus: { path: any; offset: ...

Retrieving information from a .json file using TypeScript

I am facing an issue with my Angular application. I have successfully loaded a .json file into the application, but getting stuck on accessing the data within the file. I previously asked about this problem but realized that I need help in specifically und ...

Angular 11.0.3 displaying ngClass issue (Unable to bind ngClass as it is not recognized as a property of div)

While working on an angular project, I implemented a light and dark theme using mat-slide-toggle to switch between themes. The theme is stored as a boolean called isDark in a Behavioral Subject service. There are two lazy-loaded modules - one for the home ...

Stack the labels of separate datasets on top of each bar in a bar chart using Chartjs: How can this be achieved?

chart.js 4.4.2 chartjs-plugin-datalabels I am aiming to achieve this effect const chartCtr = document.querySelector('#temp-chart1') as HTMLCanvasElement; new Chart(chartCtr, { type: 'line', plugins: [ChartDataLabels], opt ...

Issue with Angular 6 where data is not binding to the UI on initialization

I am struggling with binding dynamic data to the UI and Data tables on my website. Despite trying various methods, I have not been able to achieve success. Currently, I am using the smart admin latest theme for development. When I make a call to the API, ...

Using TypeScript to define generic types for classes, method parameters, and method return types

I am facing an issue with this particular function function getCollection<T>(collectionType: T): Collection<T> { return new Collection<T>() } In the Collection class, I have the following code snippet export class Collection<T> { ...

Middleware fails to execute on routing in Nextjs 13.4 application

Something's not quite right. I can't seem to get my middleware to run... Here's the code I'm using: export const config = { matcher: '/api/:function*', }; I specified this config so that it would run only when there's ...

This component is not compatible with JSX syntax and cannot be used as a JSX component. The type '() => Element' is not suitable for JSX element rendering

My Nextjs seems to be malfunctioning as I encountered the following error in a Parent component. Interestingly, the Spinner Component remains error-free Spinner.tsx export default function Spinner() { return ( <div className='flex ...

Is there a way to replicate the tree structure of an array of objects into a different one while modifying the copied attributes?

Is there a way to replicate the tree structure of an array of objects to another one in TypeScript, with different or fewer attributes on the cloned version? Here's an example: [ { "name":"root_1", "extradata&qu ...

Typescript Nested Class (also known as Angular Private Inner Interface)

Utilizing a Nested Interface in an Angular Directive When working in Java, I have found static nested classes to be a helpful way to structure my code. Now, when trying to do something similar in Typescript with Angular, I'm running into some challen ...

Next.js React Server Components Problem - "ReactServerComponentsIssue"

Currently grappling with an issue while implementing React Server Components in my Next.js project. The specific error message I'm facing is as follows: Failed to compile ./src\app\components\projects\slider.js ReactServerComponent ...

What are the steps to troubleshoot a Node Package Manager library in Visual Studio Code?

I have created a Typescript library that I plan to use in various NodeJS projects. The source code is included in the NPM package, so when I install it in my projects, the source also gets added to the node_modules folder. Now, during debugging, I want to ...

Tips for preventing circular dependencies in JavaScript/TypeScript

How can one effectively avoid circular dependencies? This issue has been encountered in JavaScript, but it can also arise in other programming languages. For instance, there is a module called translationService.ts where upon changing the locale, settings ...

Vue-i18n does not offer a default export option

Hello everyone! This is my first experience using vue-i18n in a project with TypeScript + Vue. Following the instructions from the official site, I installed it using yarn install vue-i18n. Next, I tried to import it into main.ts using import VueI18n from ...

Using Typescript: Generate keys in function return depending on parameter

Currently in the process of developing an SDK for a Rest API that includes an embed request parameter to fetch additional resources and add them to the response. I am exploring if there is a way, using Typescript, to extract these embed parameters while de ...

Struggling to locate the module in React Native with TypeScript configuration

Currently, I am in the middle of transitioning our react-native project from JavaScript to TypeScript. As I attempt to import old modules, I keep encountering the following error: Cannot find module 'numeral' Oddly enough, the 'numeral&apo ...

I am sorry, but it seems like there is an issue with the definition of global in

I have a requirement to transform an XML String into JSON in order to retrieve user details. The approach I am taking involves utilizing the xml2js library. Here is my TypeScript code: typescript.ts sendXML(){ console.log("Inside sendXML method") ...

What seems to be the issue with the useState hook in my React application - is it not functioning as

Currently, I am engrossed in a project where I am crafting a Select component using a newfound design pattern. The execution looks flawless, but there seems to be an issue as the useState function doesn't seem to be functioning properly. As a newcomer ...

How to update the page title in React TypeScript 16.8 without using Helmet

I have created a custom 404 not found page, and I would like the title of the page to change when someone navigates to it. Unfortunately, I do not want to use Helmet for this purpose, but I am struggling to make constructor or componentDidMount() work in ...

Attempting to categorize JSON object elements into separate arrays dynamically depending on their values

Here's the JSON data I'm currently working with: ?$where=camis%20=%2230112340%22 I plan to dynamically generate queries using different datasets, so the information will vary. My main objective is to categorize elements within this array into ...