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

Ways to specify a setter for a current object property in JavaScript

Looking to define a setter for an existing object property in JavaScript ES6? Currently, the value is directly assigned as true, but I'm interested in achieving the same using a setter. Here's a snippet of HTML: <form #Form="ngForm" novalida ...

Is it possible to detach keyboard events from mat-chip components?

Is there a way to allow editing of content within a mat-chip component? The process seems simple in HTML: <mat-chip contenteditable="true">Editable content</mat-chip> Check out the StackBlitz demo here While you can edit the content within ...

Converting language into class components using ngx-translate in Angular

Seeking to convert the information from a table into my typescript class. The data in the table is sourced from a JSON file within the /assets directory. Is there a method to accomplish this task? How can I categorize translation within a typescript class ...

Managing simultaneous asynchronous updates to the local state

There is a scenario where a series of asynchronous calls are made that read from a local state S, perform certain computations based on its current value, and return an updated value of the local state S'. All these operations occur at runtime, with ...

Is it possible to confirm that a value is a valid key without prior knowledge of the object's keys during compile-time?

Is there a way in TypeScript to declare that a variable is a keyof some Record without prior knowledge of the keys? For instance, consider an API response returning JSON data. Is it possible to define a type for the keys of this payload to ensure that whe ...

Choosing a personalized component using document selector

Currently, I am working on an application using Stenciljs and have created a custom element like this: <custom-alert alertType="warning" alertId="warningMessage" hide>Be warned</custom-alert> The challenge arises when attem ...

Navigating the world of Typescript: mastering union types and handling diverse attributes

I am currently working on building a function that can accept two different types of input. type InputA = { name: string content: string color: string } type InputB = { name: string content: number } type Input = InputA | InputB As I try to impleme ...

Transforming an array of elements into an object holding those elements

I really want to accomplish something similar to this: type Bar = { title: string; data: any; } const myBars: Bar[] = [ { title: "goodbye", data: 2, }, { title: "universe", data: "foo" } ]; funct ...

What is the significance of `new?()` in TypeScript?

Here is a snippet of code I'm working with in the TypeScript playground: interface IFoo { new?(): string; } class Foo implements IFoo { new() { return 'sss'; } } I noticed that I have to include "?" in the interface met ...

Utilizing a tuple for indexing in Typescript

Imagine you have a tuple containing keys like ["a", "b", "c"] and a nested object with these keys as properties {a: {b: {c: number}}}. How can you recursively access the members of the tuple as indices in typescript? An example implementation without prop ...

Creating a dynamic selection in Angular without duplicate values

How can I prevent repetition of values when creating a dynamic select based on an object fetched from a database? Below is the HTML code: <router-outlet></router-outlet> <hr> <div class="row"> <div class="col-xs-12"> & ...

Utilizing the spread operator in Typescript interfaces: best practices

I have a react component that includes the spread operator operating on ...other and passed down to lower levels of the component. interface ButtonProps { colourMode: string; regular: boolean; buttonText: string; disabled?: boolean; iconSize?: st ...

What should be the return type of a Jest test when written in a Typescript function?

When encapsulating a Jest test in a function with TypeScript, what is the expected return type? Thank you. const bar:ExpectedReturnType = () => test('this is another test', expect(false).toBeFalsy()); ...

Converting JSON data into an array of a particular type in Angular

My current challenge involves converting JSON data into an array of Recipe objects. Here is the response retrieved from the API: { "criteria": { "requirePictures": true, "q": null, "allowedIngredient": null, "excluded ...

Tips for handling undefined values in observable next methods to return a default error message

I sent a request over the network and received a response. Whenever I encounter an undefined value in the response, I want to return a default error message. The response may contain multiple levels of nested objects. Is there a way to replace the if else ...

Employing Typescript types in array notation for objects

Can someone please help me decipher this code snippet I found in a file? I'm completely lost as to what it is trying to accomplish. const user = rowData as NonNullable<ApiResult["getUsers"]["data"][number]["users"]> ...

Utilizing generic union types for type narrowing

I am currently attempting to define two distinct types that exhibit the following structure: type A<T> = { message: string, data: T }; type B<T> = { age: number, properties: T }; type C<T> = A<T> | B<T>; const x = {} as unkn ...

Can we determine the data type of a value within a class instance by utilizing a function to retrieve it?

Is it feasible to create a function that maintains typing and functions in the same way as this: class Example { someNumber:number = 1; someString:string = "test"; } const example = new Example(); const value = example.someNumber; // typ ...

Exploring the Possibilities of Nipplejs Integration in Vue with Quasar

Trying to implement Nipplejs in my Vue Project using quasar Components. Installed nipplejs through npm install nipplejs --save. Attempted integration of the nipple with the code snippet below: <template> <div id="joystick_zone">&l ...

I encountered an issue where TypeScript's generics function was unable to locate a property within an interface

I am attempting to define a function in typescript using generics, but I encountered the following error: "Property 'id' does not exist on type 'CustomerInterface'" This occurs at: customer.id === +id getCustomer<Custo ...