Can Typescript provide an array of the different types of values in a record?

I am interested in obtaining all the various types of values within a record. For instance, if I have an object structured like this:

{
  'a': 1,
  'b': false
}

What I aim to achieve is having a type that includes number and boolean.

I am attempting to create function mappings without compromising on types:

type MyContext = { store: number[]; };

const obj: Record<string, (context: MyContext, ...args: any[]) => void> = {
    log(ctx: MyContext, msg: string) {
        console.log(msg);
    },
    add(ctx: MyContext, value: number) {
        ctx.store.push(value);
    }
};

const convert = <TContext>(context: TContext, objectWithFunctions: Record<string, (context: TContext, ...args: any[]) => void>) => {
    //omit the first parameter and return a new object with modified functions
    const result: Record<string, (...args: any[]) => void> = {};

    Object.keys(objectWithFunctions).forEach((functionName) => {
        const func = objectWithFunctions[functionName];
        result[functionName] = (...args) => func(context, ...args);
    });

    return result;
};

const context = { store: [] };
const newObj = convert(context, obj);
newObj.log('some message');
newObj.add(12);
newObj.add(); //should give an error
newObj.foo(); //should give an error
console.log(newObj, context.store);

Playground

This current implementation is functional but lacks stricter typing because it uses any[] as a constraint for the second argument in all of the object's functions. Is there a way to infer the types somehow and return a more precise type rather than

Record<string, (...args: any[]) => void
?

Answer №1

To begin with, specifying the type of obj as

Record<string, (context: MyContext, ...args: any[]) => void>
will result in the loss of detailed information about method names and arguments in the initializer. If you want convert(context, obj) to recognize log and add, it's better not to provide an annotation for obj and let the compiler infer its type:

const obj = {
    log(ctx: MyContext, msg: string) {
        console.log(msg);
    },
    add(ctx: MyContext, value: number) {
        ctx.store.push(value);
    }
};

If calling convert(context, obj) doesn't trigger any errors from the compiler, then the type of obj is appropriate.


Next, for convert() to strongly type its output, it needs to be generic not only in T, the type of context, but also in a type parameter related to the mapping of method names and arguments in objectWithFunctions. I suggest introducing a new type parameter A, representing an object type with keys as method names and values as argument lists excluding the initial context parameter of type T.

For instance, for obj, A would be defined as:

{
    log: [msg: string];
    add: [value: number];
}

While you won't directly use a value of type

A</code, it can be inferred from the input provided to <code>objectWithFunctions
, making it convenient to represent both objectWithFunctions and the return type using A. The typings are as follows:

const convert = <T, A extends Record<keyof A, any[]>>(
    context: T,
    objectWithFunctions: { [K in keyof A]: (context: T, ...rest: A[K]) => void }
) => {
    const result = {} as { [K in keyof A]: (...args: A[K]) => void };

    (Object.keys(objectWithFunctions) as Array<keyof A>)
        .forEach(<K extends keyof A>(functionName: K) => {
            const func = objectWithFunctions[functionName];
            result[functionName] = (...args) => func(context, ...args);
        });

    return result;
};

Therefore, the type of objectWithFunctions is a mapped type where the array A[K] associated with each property K</code of <code>A is transformed into a function type comprising an argument of type

T</code followed by a rest argument of type <code>A[K]
. It happens because this type conforms to the pattern {[K in keyof A]...}, which is a homomorphic mapped type allowing efficient inference from such types. The return type, as indicated in the annotation of result, mirrors the same mapped type but without the initial T argument.

Some type assertions were necessary within the implementation of the function to inform the compiler about certain values' types. Since the initial value of result is an empty object, an assertion was required to ensure that it would match the final type. Additionally, given that the return type of Object.keys() is simply

string[]</code, another assertion ensured that it returns an array of <code>keyof A
.


Let's put this approach to the test:

const context: MyContext = { store: [] }; 

Annotating context as

MyContext</code here is essential to prevent the compiler from assuming that <code>store
will always be an empty array (never[]). Here's how we apply this concept:

const newObj = convert(context, obj);        

You can leverage Quickinfo via IntelliSense to observe that invoking convert() infers MyContext for

T</code and the specified <code>{ log: [msg: string];  add: [value: number];}
type for A:

/* const convert: <MyContext, {
    log: [msg: string];
    add: [value: number];
}>(context: MyContext, objectWithFunctions: {
    log: (context: MyContext, msg: string) => void;
    add: (context: MyContext, value: number) => void;
}) => {
    ...;
} */

Upon inspecting myObj, you'll notice that it aligns perfectly with your desired type:

/* const newObj: {
    log: (msg: string) => void;
    add: (value: number) => void;
} */

This indicates that the operation functions as intended:

newObj.log('some message');
newObj.add(12);
newObj.add(); // error
newObj.foo(); // error
console.log(newObj, context.store); // {}, [12]

Access the link to play around with the code on TypeScript Playground.

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 an error code TS5055 when attempting to call an API within a TypeScript constructor

I'm attempting to retrieve a list of customers by calling a GET API from a dashboard constructor as shown below: // tslint:disable-next-line:max-line-length constructor(public addCustomerDialog: MatDialog, private router: Router, private rout ...

Create a simulated constructor to generate an error

Currently, I am faced with a challenge of writing a test that is expected to fail when trying to instantiate the S3Client object. It seems like Vitest (similar to Jest) replaces the constructor with its own version when mocked, preventing my original const ...

Angular 2: Store all form inputs within a JSON object upon submission

I am working on a form that has multiple fields and I need to retrieve the data once it is submitted. This is the code in component.html : <div class="ui raised segment"> <h2 class="ui header">Demo Form: Sku</h2> <form #f="ngFor ...

How can I combine multiple requests in RxJS, executing one request at a time in parallel, and receiving a single combined result?

For instance, assume I have 2 API services that return data in the form of Observables. function add(row) { let r = Math.ceil(Math.random() * 2000); let k = row + 1; return timer(r).pipe(mapTo(k)); } function multiple(row) { let r = Math.c ...

Encountering a conflict while extending an interface with react-final-form's FieldRenderProps in a TypeScript project

Hey There! Recently, I've been working on creating a customized react function component to pass to react-final-form Field. However, I've encountered an error. Let's Dive Into the Details Firstly, here's a snippet of the form: impor ...

The process of dynamically adding a property to the parameter of the React setState method

In my project, I utilize reactjs and typescript. Here are the lines of code I am using: if (reinitGridState) { this.setState({ isbusy: false, itemsList: data, colList: columns, gridState: this.initGridState, gri ...

The issue with Cypress AzureAD login is that it consistently redirects users away from the intended Cypress window

Trying to incorporate the automation of Azure AD login using Cypress, I have been facing some challenges. Even after configuring the local session storage according to the instructions in this GitHub repository https://github.com/juunas11/AzureAdUiTestAu ...

Unable to locate control with undefined name attribute in Angular version 14

I'm encountering the following issue. https://i.sstatic.net/fV1D1.png When I click on the area indicated by the BLUE ARROW in the image above, it leads to the of of ngFor loop in my html template. <div class="w-[60%]"> &l ...

I'm working on an Angular2 project and I'm looking for a way to concatenate all my JavaScript files that were created from TypeScript in Gulp and then include them in my index

How can I concatenate all JavaScript files generated from typescript in my Angular2 project with Gulp, and then add them to my index.html file? I am using Angular2, typescript, and gulp, but currently, I am not concatenating the javascript files it genera ...

Consolidate and arrange the results of several HTTP requests to a single endpoint

My goal is to send multiple requests to an API, merge and sort the results, and then assign them to the this.products$ observable. The current code mostly accomplishes this by combining all the results into one list. The fetchIds() method retrieves parame ...

How to Retrieve Multiple Toggle Button Values using ID in AngularJS?

I am looking to retrieve the value of a toggle button as either yes or no. Since numerous questions are being generated dynamically, I need to determine this based on item.id. I am utilizing Angular and need to implement the logic in a TS file. Any assista ...

Encountering an issue with Typescript and SystemJS: Struggling to locate a

After developing a module, I decided to move it out of the app and into node_modules. However, I encountered an error error TS2307: Cannot find module 'bipartite-graph'.. In this case, bipartite-graph is actually my own module. Here is the conte ...

What method can be used to modify the src attribute of an <img> tag given that the id of the <img> element is known?

My challenge involves displaying an array of images using a *ngFor loop. itemimg.component.html <div *ngFor="let itemimg of itemimgs" [class.selected]="itemimg === selectedItemimg" (click)="onSelect(itemimg)"> <img id="{{itemim ...

Show dynamic JSON data in a nested format on the user interface with Aurelia's Treeview component

In the visual representation provided, there are currently three objects in the array. These objects, referred to as "parents", each have their own set of "children". The complexity lies in the fact that a parent element can also serve as a child element w ...

What steps can I take to set a strict boundary for displaying the address closer to the current location?

While the autocomplete feature works perfectly for me, I encountered an issue where it suggests directions away from my current location when I start typing. I came across another code snippet that uses plain JavaScript to solve this problem by setting bou ...

Error: ngModel does not reflect dynamic changes in value

After calling a Spring service, I am receiving JSON data which is stored in the "etapaData" variable. 0: id: 266 aplicacao: {id: 192, nome: "Sistema de Cadastro", checked: false} erro: {id: 220, nome: "Falta de orçamento", checked: false} perfil: {id: 8, ...

Having trouble compiling the Electron App because of a parser error

Struggling to set up a basic electron app using Vue 3 and Typescript. Following the successful execution of certain commands: vue create app_name cd .\app_name\ vue add electron-builder npm run electron:serve Encountering issues when trying to i ...

Enums are not recognized by TypeScript when used within an array

I have defined an enum as follows: export enum Roles { ADMIN, NONE; } An object is being used which utilizes this enum. The structure of the object is: export interface User { name: string; roles: Roles[]; } Upon fetching this object via a web r ...

"Using Angular and TypeScript to dynamically show or hide tabs based on the selected language on a website

When switching the language on the website, I want to display or hide a specific tab. If the language is set to German, then show the tab; if any other language is selected, hide it. Here's my code: ngOnInit(): void { this.translate.onLangChange.s ...

Setting up in the namespace for typescript

Is there a way to assign to namespaces using dot notation like this? namespace item {} item.item1 = { name: "Some Item" } item.item2 = { name: "Some Item" } An error is thrown with: Property 'item1' does not exist on ty ...