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

Cross-origin resource sharing (CORS) seems to be creating a barrier for the communication between my Angular

During the process of developing an Angular and NestJS app with NGXS for state management, I encountered a CORS error while serving my application. The error message in the console indicated: Access to XMLHttpRequest at 'localhost:333/api/product-i ...

Integrating one service into another allows for seamless access to the imported service's properties and methods within an Angular

After reviewing the content at https://angular.io/guide/dependency-injection-in-action, it appears that what I am trying to achieve should be feasible. However, I encounter this error when attempting to invoke a method on my injected service from another s ...

Angular 2 decorators grant access to private class members

Take a look at this piece of code: export class Character { constructor(private id: number, private name: string) {} } @Component({ selector: 'my-app', template: '<h1>{{title}}</h1><h2>{{character.name}} detai ...

Tips for centering an Angular mat prefix next to a label in a form field

Hey everyone, I need some help with aligning the prefix for an input with the mat label. Can anyone suggest a way to adjust the mat prefix so that it lines up perfectly with the mat label? Any assistance or ideas would be greatly appreciated. Here is the ...

Testing Playwright - accessing variables from the .env file

I am currently working on a Playwright test script in TypeScript and I'm looking for a way to leverage variables from my .env file within the test. Does anyone know how this can be accomplished? ...

When defining properties/data in Vue mixins, the properties/data of the mixin are not accessible

A vue mixin is being used to store information (referred as `world` in the example below) that needs to be accessed in multiple vue components without having to import it every time. Check out the code snippet: <template> <ol> <li> ...

The default value of components in Next.js

I'm working on establishing a global variable that all components are initially rendered with and setting the default value, but I'm unsure about how to accomplish the second part. Currently, this is what I have in my _app.tsx: import { AppProps ...

Unable to run unit tests on project using my custom React library

If any of you have encountered this issue or know how to solve it, please help me. I created an NPM package that can be found at https://www.npmjs.com/package/@applaudo/react-clapp-ui It installs and runs smoothly in other projects using create react app; ...

Using the css function within styled-components

Struggling with implementing the media templates example from the documentation and figuring out how to type the arguments for the css function in plain JS: const sizes = { desktop: 992 } const media = Object.keys(sizes).reduce((acc, label) => { ...

There seems to be an issue with my React application that was built using Webpack 5 and compiled with TypeScript. The @tailwind directive is not functioning properly in the browser, and

As I embark on creating a fresh react application using Webpack 5, Tailwind CSS, and Typescript, I find myself at a crossroads. Despite piecing together various tutorials, I am struggling to configure the postcss-loader for Tailwind. While traditional .css ...

How to use TypeScript variables in React applications

In my current project, I am attempting to customize a Fabric JS component (Dropdown) using styled-components within a React component. The specific CSS class names are defined in a file located at office-ui-fabric-react/lib/components/Dropdown/Dropdown.sc ...

Tips for enabling custom object properties in Chrome DevTools

In my typescript class, I am utilizing a Proxy to intercept and dispatch on get and set operations. The functionality is working smoothly and I have successfully enabled auto-completion in vscode for these properties. However, when I switch to the chrome d ...

Navigating through JSON object using Angular 2's ngFor iterator

I want to test the front end with some dummy JSON before I write a service to get real JSON data. What is the correct way to iterate through JSON using ngFor? In my component.ts file (ngOnInit()), I tried the following code with a simple interface: var js ...

Using Typescript to extract elements from one array and create a new array

I have a set of elements "inputData" , and it appears as follows : [{code:"11" , name= "test1" , state:"active" , flag:"stat"}, {code:"145" , name= "test2" , state:"inactive" , flag:"pass"}, {code1:"785" , name= "test3" , state:"active" , flag:"stat"}, .. ...

Retrieve information from a URL to transmit to a different page in NextJS using Typescript and AppRouter

I'm struggling with transitioning from the Home page to the Detail page. I've successfully passed data to the URL from the Home screen, but I'm having trouble accessing it in the Detail screen. I'm working with NextJS ver 13, using Type ...

Can a React.tsx project be developed as a standalone application?

As a student, I have a question to ask. My school project involves creating a program that performs specific tasks related to boats. We are all most comfortable with React.tsx as the programming language, but we are unsure if it is possible to create a st ...

Why does my Visual Studio Code always display "building" when I launch an extension?

https://code.visualstudio.com/api/get-started/your-first-extension I followed a tutorial to create a hello world extension. Why does my VSCode always display 'building' when I run the extension? Executing task: npm run watch < [email p ...

Efficiently Updating Property Values in Objects Using TypeScript and Loops

I have been looking into how to iterate or loop through a code snippet, and while I managed to do that, I am facing an issue where I cannot change the value of a property. Here is the snippet of code: export interface BaseOnTotalPaidFields { groupName ...

Transform webservice data into TypeScript object format, ensuring mapping of objects from capital letters to camel case

Something peculiar caught my attention in my Angular2 TypeScript project. When objects are fetched from a web service, they have the type "Level" and the properties are in Pascal case. However, during runtime, I noticed that the properties of these Levels ...

Collaborate and apply coding principles across both Android and web platforms

Currently, I am developing a web version for my Android app. Within the app, there are numerous utility files such as a class that formats strings in a specific manner. I am wondering if there is a way to write this functionality once and use it on both ...