Explain the object type that is returned when a function accepts either an array of object keys or an object filled with string values

I've written a function called getParameters that can take either an array of parameter names or an object as input. The purpose of this function is to fetch parameter values based on the provided parameter names and return them in a key-value object format where the key represents the parameter name and the value is a string type.

Here's how the function looks along with an example of its usage:

async function getParameters<T extends Array<string> | RecursiveObject>(
  paramNames: T,
): Promise<ParamValuesDictionary> {
  const paramNamesArr = !Array.isArray(paramNames)
    ? toFlatArray(paramNames)
    : paramNames as Array<string>;

  // Stub implementation of loading parameters
  const loadedParams: Parameter[] = paramNamesArr.map(name => ({
    name: name,
    val: `${name}_val`,
  }));
  
  const paramValues = convertToDictionary(loadedParams);
  return paramValues;
}

function toFlatArray(obj: RecursiveObject): string[] {
  let values: string[] = [];
  for (const value of Object.values(obj)) {
    if (typeof value === 'object') {
      values = values.concat(toFlatArray(value));
    } else {
      values.push(value);
    }
  }
  return values;
}

function convertToDictionary(
  parameters: Parameter[],
): ParamValuesDictionary {
  const paramValues: ParamValuesDictionary = parameters.reduce(
    (acc, { name, val }) => {
      acc[name] = val;
      return acc;
    },
    {} as ParamValuesDictionary,
  );
  return paramValues;
}

type Parameter = {
  name: string;
  val: string;
};
type RecursiveObject = {
  [key: string]: string | RecursiveObject;
};
type ParamValuesDictionary = { [name: string]: string };

getParameters(['a', 'b']).then(parameters => {
    console.log('Using an array:', parameters);
    /* OUTPUT:
    "Using an array:",  {
      "a": "a_val",
      "b": "b_val"
    } 
    */
});

getParameters({
    nameA: 'a',
    nameB: 'b',
    namesC: {
        nameC1: 'c1',
        nameC2: 'c2',
    },
}).then(parameters => {
    console.log('Using an object:', parameters);
    /* OUTPUT:
    "Using an object:",  {
      "a": "a_val",
      "b": "b_val",
      "c1": "c1_val",
      "c2": "c2_val"
    } 
    */
});

The current result of the getParameters function is a ParamValuesDictionary, which allows any string as a key. I would like to have strict keys in the ParamValuesDictionary based on the provided paramNames function argument. How can I modify the type ParamValuesDictionary to achieve this behavior? Appreciate any guidance.

TS Playground link available here.

Answer №1

When invoking getParameters(parameters), where the parameter parameters is a generic type denoted as T and restricted to either a RecursiveObject or an array of strings, the desired output should be an object with all string values and keys determined by T in a specific manner. This can be achieved through the operation called Keys<T>. Therefore, the call signature for getParameters() should be as follows:

declare function getParameters<const T extends readonly string[] | RecursiveObject>(
  paramNames: T,
): Promise<{ [K in Keys<T>]: string }>;

To ensure accurate inference, the const type parameter modifier is used on T. Without this, a value like {k1: "v1"} might be inferred simply as {k1: string}, which may not meet the requirements. By keeping track of the literal types of property values, it ensures more specificity in the output.

This necessitates changing from string[] to readonly string[], as the ReadonlyArray type aligns better with inferring readonly tuples for array literals when using the const modifier.

The next step involves defining Keys<T>:


If T is a subtype of RecursiveObject, then Keys<T> should represent the union of the string literal types found in its leaf nodes. This requires a recursive utility type named RecursiveObjectLeaves<T>. Otherwise, if T is an array of string literal elements, we simply extract the union of those elements by indexing into T with number. The structure of Keys<T> appears as follows:

type Keys<T extends readonly string[] | RecursiveObject> =
  T extends RecursiveObject ? RecursiveObjectLeaves<T> :
  T extends readonly string[] ? T[number] : never

Next, let's define RecursiveObjectLeaves<T>.


One approach to calculating this would be:

type RecursiveObjectLeaves<T> =
  T extends RecursiveObject ?
  { [K in keyof T]: RecursiveObjectLeaves<T[K]> }[keyof T]
  : Extract<T, string>

Essentially, this conditional type checks if T is an object, recursively calls RecursiveObjectLeaves on each property, and generates the union within a distributive object type. For instances where T is a leaf node, it retrieves the element as a string. This is done using the Extract utility type.

To validate this implementation, consider the test conducted below:

type ROLTest = RecursiveObjectLeaves<{
  k1: "v1",
  k2: {
    k3: "v3",
    k4: "v4",
    k5: { k6: { k7: { k8: "v8" } } }
  }
}>;
// Expected output: "v1" | "v3" | "v4" | "v8"

The results display the expected outcome.


Combining these components, type assertions are employed within the implementation of getParameters() to circumvent compiler errors. The complexity of returning a value that adheres to the type

Promise<{ [K in Keys<T>]: string }></code for generic <code>T
goes beyond the compiler's capacity. Hence, manual type assertion is utilized:

async function getParameters<const T extends readonly string[] | RecursiveObject>(
  paramNames: T,
): Promise<{ [K in Keys<T>]: string }> {

  ⋯

  return paramValues as any;
}

To observe how this functions in practice, example tests have been included:

getParameters(['a', 'b']).
  then(parameters => {
    console.log('Passed array:', parameters);
  });

getParameters({
  nameA: 'a',
  nameB: 'b',
  namesC: {
    nameC1: 'c1',
    nameC2: 'c2',
  },
}).then(parameters => {
  console.log('Passed object:', parameters);
});

The outcomes showcase the compiler's ability to retrieve the literal types accurately, demonstrating the effectiveness of the implemented logic.

Playground link

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

Exploring ways to interact with an API using arrays through interfaces in Angular CLI

I am currently utilizing Angular 7 and I have a REST API that provides the following data: {"Plate":"MIN123","Certifications":[{"File":"KIO","Date":"12-02-2018","Number":1},{"File":"KIO","Date":"12-02-2018","Number":1},{"File":"preventive","StartDate":"06 ...

Implementing express-openid-connect in a TypeScript project

Trying to incorporate the express-openid-connect library for authentication backend setup with a "simple configuration," an issue arises when attempting to access the oidc object from express.Request: app.get("/", (req: express.Request, res: express.Respon ...

Retrieving Data in Typescript Async Function: Ensuring Data is Returned Once All Code is Executed

I need help with waiting for data to be retrieved before returning it. The code below fetches data from indexedDB and sends it back to a component. I understand that observables or promises can accomplish this, but I am struggling with how to implement t ...

Discover the process of retrieving all workday dates using Angular

Currently, I am working on a project in Angular that involves allowing employees to record their work hours. However, I am facing a challenge in figuring out how to gather all the work dates and store them in an array. Here is what I have attempted so fa ...

Possibility for Automatic Type Inference in Generics

Is there a way to have a method infer the type of function parameter without specifying its generic? Currently it is 'GET' | 'POST', but I only need the literal 'GET' const func = <Params, Method extends "GET" | & ...

How can I utilize the Redux store outside of a component in a React application with ASP.NET Core and TypeScript?

While I have some experience with React, I am new to using Redux. Luckily, Visual Studio 2017 comes with a built-in template for React + Redux under .NET Core 2.0. About my environment: Visual Studio 2017 React 15.6.1 TypeScript 2.4.1 Redux 3.7.1 Rea ...

Acquiring information from a Service and saving it in a Child component - Angular version 11

Utilizing my service, I fetch API data for the child component. Initially, it retrieves the Id and other user data, displaying it in the console. ngOnInit(): void { this.commonService.userSetChange.subscribe( apiData => { this.getUserS ...

Attempting to invoke a promise within a function yields an error message stating that it lacks call signatures

Recently, I came across this interesting class: export class ExponentialBackoffUtils { public static retry(promise: Promise<any>, maxRetries: number, onRetry?: Function) { function waitFor(milliseconds: number) { return new Pr ...

There was an error linking the module "electron_common_features" which caused the Electron react test to fail

I recently developed a React Electron application using the electron-react-boilerplate template. I also added the @electron/remote package and made changes to the test case. However, upon running the command npm test, an error message stating No such modul ...

Ways to initiate update notification when altering an array object?

I am working on my Angular4 app and I have a component that uses a *ngFor directive to iterate over an array: <div *ngFor="let person of persons"> {{person.name}} {{person.car}} </div> Within the same component, there is a feature to ...

Tips on utilizing index and eliminating React Warning: Ensure every child within a list has a distinct "key" prop

Hello, I am encountering an issue where I need to properly pass the index in this component. Can you help me figure out how to do that? Error: react-jsx-dev-runtime.development.js:117 Warning: Each child in a list should have a unique "key" prop ...

Obtaining undefined values for req and resolvedUrl in GetServerSideProps function

In my project, I am currently using next.js version ""next": "^12.1.4"" and node version ""@types/node": "^14.14.6". I have created a function called getServerSideProps with parameters req and resolvedUrl. When the ...

What is the most effective method for identifying duplicate values in a multidimensional array using typescript or javascript?

I have a 2D array as shown below: array = [ [ 1, 1 ], [ 1, 2 ], [ 1, 1 ], [ 2, 3 ] ] I am looking to compare the values in the array indexes to check for duplicates. For example array[0] = [1,1]; array[1] = [1,2]; array[2] = [1,1]; We can see that ...

Clicking a button in React requires two clicks to update a boolean state by triggering the onClick event

I have a React functional component with input fields, a button, and a tooltip. The tooltip is initially disabled and should only be enabled and visible when the button is clicked and the input fields contain invalid values. The issue I'm facing is t ...

React Typescript: exploring the power of dynamic types

Can dynamic typing be implemented? The JSON structure I am working with looks like this: { "fieldName": "Some text", "type": String, "inputType": "text" }, { "fieldName": "Some bool&q ...

Error Message: Angular 5 with SignalR (DefinitelyTyped) - '$' Not Found in Typings

I am currently working on implementing SignalR into my Angular 5 application. To do this, I added the following TypeScript type definitions from DefinitelyTyped: npm install --save @types/jquery npm install --save @types/signalr The version of Typescrip ...

Encountering issues with Typescript when providing parameters for res.status().json()

I've recently started using Typescript and I'm still in the learning process by converting some existing JS code to TS. In my code: res.status(200).json({ user: data.user }) I encountered a red squiggly underline under user:data.user ...

employing a parameterized type to accommodate a combination of two diverse items as input

I am facing a challenge with a simple use case and have been unable to find an example that covers it adequately. The situation is this: I have a function that should accept two different objects that are very similar. Therefore, I want to use a generic t ...

Having trouble utilizing a function with an async onload method within a service in Angular - why does the same function work flawlessly in a component?

I successfully created a component in Angular that can import an Excel file, convert it into an array, and display its content as a table on the page. The current implementation within the component looks like this: data-import.compoent.ts import { Compo ...

Setting up roles and permissions for the admin user in Strapi v4 during the bootstrap process

This project is built using Typescript. To streamline the development process, all data needs to be bootstrapped in advance so that new team members working on the project do not have to manually insert data into the strapi admin panel. While inserting ne ...