Universal key and attribute retrieval function

Currently, I am attempting to analyze the characteristics of objects within arrays. In order to maintain type safety, I am utilizing a getter function to access the child object of the array objects (if necessary) that contains the specific property to be observed.

The propertyName / key must be a string since the observe library I am utilizing requires it.

The getter should have the capability to accept a function that returns the same type that was initially provided, such as o => o.

Below is a concise example:

function example<A, B>(
    array: A[], 
    getter: (ao: A) => B, 
    key: keyof B, 
    callback: (value: B[keyof B]) => void
) {
    callback(getter(array[0])[key]);
}

example([{b: {c: 1}}], a => a.b, "c", v => {});

Unfortunately, this results in the error message:

Argument of type '"c"' is not assignable to parameter of type 'never'.

However, the following example does function correctly:

function alternate<A>(
    array: A[], 
    key: keyof A, 
    callback: (value: A[keyof A]) => void
) {
    callback(array[0][key]);
}

alternate([{b: 1}], "b", v => {});

Why is the compiler struggling to infer the type of B and are there any potential workarounds I could implement?

Answer №1

The reason why the compiler doesn't infer B is not entirely clear to me. However, based on my intuition, it seems that the compiler finds it easier to infer the type of a parameter that is directly passed to a function rather than inferring a type related to the passed-in parameter. As a solution, it might be helpful to replace B with Record<K,V>, where K represents the actual key passed in and V represents the value type associated with that key.

For those unfamiliar, Record<K,V> is a part of the TypeScript standard library and is defined as:

type Record<K extends string, T> = {
    [P in K]: T; 
}

With this change, the function would look like:

function foo<A, K extends string, V>(
    array: A[], 
    getter: (ao: A) => Record<K,V>, 
    key: K, 
    callback: (value: V) => void
) {
    callback(getter(array[0])[key]);
}

Running this function with specific parameters like:

foo([{ b: { c: 1 } }], a => a.b, "c", v => { });

Correctly infers the parameters as

<{ b: { c: number; }; }, "c", number>
.

Hopefully, this explanation is helpful. Best of luck!


Update

After evaluating a specific call:

foo([{ b: { c: 1, d: "" } }], a => a.b, "c", v => { });

The inferred types were:

foo<{ b: { c: number; d: string; }; }, "c" | "d", string | number>(...)

To address this issue where you want K to be just "c", you can adjust the function slightly by lowering the priority of inferences as shown below:

function foo<A, K extends string, V>(
  array: A[],
  getter: (ao: A) => Record<K & {}, V>, 
  key: K, 
  callback: (value: V) => void
) {
  callback(getter(array[0])[key]);
}

This adjustment allows for correct inference when calling:

foo([{ b: { c: 1, d: "" } }], a => a.b, "c", v => { });

Which now infers as

foo<{ b: { c: number; d: string; }; }, "c", {}>(...)

Any other scenarios we need to account for?

Answer №2

Like @jcalz, my approach doesn't rely on a specific reason, but rather a workaround. By breaking it down into two separate calls, you can first set A and B, then in the second call, provide the key and callback to ensure correct inference of types.

function foo2<A, B>(
    array: A[],
    getter: (ao: A) => B,
) {
    return function <K extends keyof B>(key: K, callback: (value: B[K]) => void) {
        callback(getter(array[0])[key]);
    }
}

foo2([{ b: { c: 1, d: "" } }], a => a.b)("d", v => { }); // v is string
foo2([{ b: { c: 1, d: "" } }], a => a.b)("c", v => { }); // v is number

Although the syntax may be a bit clunkier, this method ensures complete type safety.

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

Leverage the same JSDoc comment across multiple TypeScript interfaces

If I have the interfaces below: interface Foo { /** * A date string in the format `yyyy-MM-dd` */ archiveDate: string; } interface Bar { /** * A date string in the format `yyyy-MM-dd` */ publishDate: string; } The JSDoc descriptions f ...

Tips for aligning the types of objects transmitted from a Web API backend to a React/Redux frontend

After creating a backend for my app using .net, I now have CRUD operations available to me. When performing a POST action, the response includes an entire new item object: {"Id":"7575c661-a40b-4161-b535-bd332edccc71","Text":"as","CreatedAt":"2018-09-13T15 ...

Testing Jasmine with objects that contain optional properties

In the IData interface, there are optional properties available. interface IData { prop1: string, prop2?: string } setObj(){ prop1 = 'abc'; prop2 = 'xyz'; let obj1 : IData = { prop1: this.prop1, ...

Exploring the integration of React.Components with apollo-client and TypeScript

I am in the process of creating a basic React component using apollo-client alongside TypeScript. This particular component is responsible for fetching a list of articles and displaying them. Here's the code: import * as React from 'react' ...

Execute a function that handles errors

I have a specific element that I would like to display in the event of an error while executing a graphql query (using Apollo's onError): export const ErrorContainer: React.FunctionComponent = () => { console.log('running container') ...

Is there a way to turn off tsc pretty printing using the configuration file?

My typescript program is intentionally broken but I want to fix it. 12:17:23:~/hello $ cat hello.ts console.log("Hello World" 12:17:29:~/hello $ cat package.json { "dependencies": { "typescript": "^5.2.2" ...

Extend the row of the table according to the drop-down menu choice

I am working on a feature where a dropdown menu controls the expansion of rows in a table. Depending on the option selected from the dropdown, different levels of items need to be displayed in the table. For example, selecting level 1 will expand the first ...

What's the best way to include various type dependencies in a TypeScript project?

Is there a more efficient way to add types for all dependencies in my project without having to do it manually for each one? Perhaps there is a tool or binary I can use to install all types at once based on the dependencies listed in package.json file. I ...

The button event listener in React fails to trigger without a page refresh

Within my index.html file, I have included the following code snippet: <head> ... <script type="text/javascript" src="https://mysrc.com/something.js&collectorId=f8n0soi9" </script> <script ...

Error: Unable to attach the "identity" property as the object does not support extension

I encountered a simple TypeError while attempting to format my POST body. Below is the function I am using for handleSubmit : const handleSubmit = (values: any, formikHelpers: FormikHelpers<any>) => { const prepareBody = { ...values.customerC ...

What is the process for creating an additional username in the database?

As a beginner frontend trainee, I have been tasked with configuring my project on node-typescript-koa-rest. Despite my best efforts, I encountered an error. To set up the project, I added objection.js and knex.js to the existing repository and installed P ...

Make leaflet function operate synchronously

There seems to be an issue with calling the setMarker() function within another function, as the markers are not being set. It's possible that this is due to the asynchronous nature of the setMarker() function because of the Promise it uses. getCities ...

The role of callback functions in TypeScript

As I embark on my journey with Angular 2 and TypeScript, one concept that has me a bit puzzled is how to implement callback functions. I understand that this might be a basic question, but when I look at this typical JavaScript code: someOnject.doSomethin ...

What is the best way to invoke a function in a functional React component from a different functional React component?

I need to access the showDrawer method of one functional component in another, which acts as a wrapper. What are some best practices for achieving this? Any suggestions or insights would be appreciated! const TopSide = () => { const [visible, se ...

Troubleshooting Puppeteer compatibility issues when using TypeScript and esModuleInterop

When attempting to use puppeteer with TypeScript and setting esModuleInterop=true in tsconfig.json, an error occurs stating puppeteer.launch is not a function If I try to import puppeteer using import * as puppeteer from "puppeteer" My questi ...

Angular tabs display the initial tab

I attempted to implement the Tabs feature from angular material by following the provided documentation. However, I encountered an issue where the first tab does not display upon page load; I have to manually click on it to view its content. For more info ...

Exploring ways to exclude a column in a TypeORM entity while also providing the flexibility to make it optional for retrieval using the Find method

import {Entity, PrimaryGeneratedColumn, Column} from "typeorm"; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; } i prefer not to include the password here as I want it to be returned to the client: ...

The parameter must be of type 'string', but you are attempting to assign a 'Promise<any>'

Starting a React App requires fetching the user's information from localStorage and initiating a socket connection with the server based on the user's id. However, when trying to pass the user's id to the socket function, TypeScript throws ...

Dealing with Uncaught Promises in Angular 2 while injecting a service

I followed the instructions in the official tutorial to start a project, but I'm encountering an issue with injecting services into my Angular2 app. Everything was working fine until I added a service. Here are the files : app.component.ts import ...

"Learn the process of integrating Javascript files from the Angular assets folder into a specific Angular component or module (such as Angular 2, 4,

I have custom1.js, custom2.js, and custom3.js JavaScript files that I need to load into Angular components component1, component2, and component3 respectively. Instead of adding these files to the index.html globally, I want to load them specifically for e ...