Ways to declare the function prototype using object and key as parameters

I am currently working on defining a utility function that will handle axios errors and store the resulting error message into a specific field of a specified object.

The desired syntax for using this function is:

axios.get(...).then(...).catch(ParseIntoErrorString(myClass, 'loadError');

In this code snippet, myClass represents a TypeScript class containing a method that invokes this function (likely within a 'loadData' method). In most cases, myClass may be replaced with 'this'.

The 'loadError' property is a string attribute within the myClass. This concept is inspired by the behavior of jest.spyOn, where you specify the target object to spy on and then indicate the function name to replace with a spy function. It's worth noting that jest.spyOn triggers an error if a non-function string is provided.

Despite my efforts, I'm encountering issues with the following implementation:

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K }[keyof T] &
  string;

export function ParseErrorIntoString<T extends {}, P extends NonFunctionPropertyNames<Required<T>>>(obj: T, member: P): (reason: AxiosError) => void {
  return (reason: AxiosError): void => {
    // eslint-disable-next-line no-param-reassign
    obj[member] = AjaxParseError(reason);
  };
}

The NonFunctionPropertyNames portion has been directly extracted from the jest typings file.

Moreover, outside of failing at the calling site, this code raises two distinct errors:

  1. An error stating that one cannot use extends {} suggests replacing it with Record<string, unknown>. However, upon making this adjustment, passing this as an argument becomes problematic as it doesn't conform to Record<string, unknown>, though the reasoning isn't explicitly outlined.
  2. There's an error when trying to assign a string to obj[member], possibly due to the lack of restrictions within NonFunctionPropertyNames to allow only string properties.

I've also explored the following approach:

export function ParseErrorIntoString<T extends object>(obj: T, member: Extract<keyof T, string | null>): (reason: AxiosError) => void {
  return (reason: AxiosError): void => {
    // eslint-disable-next-line no-param-reassign
    obj[member] = AjaxParseError(reason);
  };
}

My assumption was that

Extract<keyof T, string | null>
would isolate keys within T bearing the type "string | null". Unfortunately, it proved to be too lenient, allowing boolean members without triggering an error when they were expected.

This laxness manifests during the assignment line obj[member] = ..., manifesting as a type mismatch warning regarding assigning a string value to obj[member].

To make matters worse, explicit declaration of T is required at the calling site for proper member acceptance—an unexpected deviation from the behavior exhibited by spyOn in jest.

Lastly, consider this self-contained example representing a third attempt nearing successful resolution:

class cls {
  public str: string | null;
  public bln: boolean;

  constructor() {
    this.str = "";
    this.bln = false;
  }
}
   
const instance = new cls();

type StringPropertyNames<T> = { [K in keyof T]: T[K] extends string | null ? K : never }[keyof T] & string;

function f<T extends cls>(obj: T, p: StringPropertyNames<T>): void {
  obj[p] = "done";
}

f(instance, 'str'); // Calling f with the instance and property works fine
console.log(instance.str) // Outputs 'done'

In this scenario, f mirrors the original ParseErrorIntoString function. Notably, invoking f(instance, 'str') produces the expected outcome, whereas attempting f(instance, 'bln') correctly raises an error due to 'bln' being incompatible with the type string.

However, I'm still faced with the persistent error message: "Type 'string' is not assignable to type T[StringPropertyNames<T>]" at the point of obj[p] = 'done'. Possibly, refining the return type of StringPropertyNames<T> could resolve this issue.

If anyone can suggest appropriate typing for the ParseErrorIntoString function, it would be greatly appreciated!

Answer №1

You have encountered an existing limitation or missing feature in TypeScript, which is detailed at microsoft/TypeScript#48992. You are trying to use

KeysOfType<T, string | null>
to represent "the keys of type T whose properties are of type string | null" and then utilize these keys to interact with values of type T that contain string | null properties. While there are workarounds for specific types of T, the compiler struggles to follow this logic when generics are involved, leading to difficulties in reading/writing properties as intended.

Until a native KeysOfType functionality that the compiler can understand is implemented, one common workaround involves reversing the constraint. Instead of defining a generic object type T with keys of type

KeysOfType<T, string | null>
, define a generic keylike type K and then construct your object type based on it such as Record<K, string | null> (utilizing the Record<K, V> utility type which specifies a type with keys of type K and values of type V). The compiler understands that
Record<K, string | null>[K]
aligns with string | null:

function f<K extends string>(
    obj: Record<K, string | null>,
    p: K
): void {
    obj[p] = "done"; // valid operation
}

This approach also works with example code like:

f(instance, 'str'); // valid

For additional considerations, you may encounter quirks related to the difference between read and write operations (focusing on writing "done" to

obj[p]</code rather than <code>string | null
, but challenging to express this concept in a compiler-friendly manner) and excess property checks (if using f({...}, 'str') with an object literal {...}, the compiler might raise concerns about non-str properties). These areas go beyond the scope of the initial inquiry.

Link to Playground for testing code

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

Challenging Issue: "The 'any' type cannot be assigned to the 'never' type"

Currently facing a challenging Typescript problem that has me puzzled. The issue arises at the line themeToChange[tileId][key] = value; The error message states Type 'any' is not assignable to type 'never' This error is directly rela ...

React: The Material-UI autocomplete input, controlled with the React Hook Form `<controller>` component, experiences issues when the `multiple` prop is set to `true`

Currently facing challenges with managing an autocomplete MUI component using the <controller> component in react-hook-form. Take a look at the code snippet below: <Controller control={control} name="rooms" render={({ field }) =&g ...

Join the Observable and formControl in Angular 4 by subscribing

My goal is to display the data retrieved from FireStore in the screen fields upon loading. However, the buildForm() function is being called before subscribing to the data, resulting in the failure to populate the screen fields with the FireStore data. pe ...

Sending an array of functions to the onClick event of a button

Having difficulty with TypeScript/JavaScript Currently working with an array of functions like this private listeners: ((name: string) => void)[] = []; Successfully adding functions to the array within another function. Now looking to trigger those ...

Upon clicking a button, I aim to retrieve the data from the rows that the user has checked. I am currently unsure of how to iterate through the rows in my mat-table

My goal is to iterate through the column of my mat-table to identify the rows that have been checked, and then store the data of those rows in an array. <td mat-cell *matCellDef="let row"> <mat-checkbox (click)="$event.stopPropagation()" (c ...

Having trouble adding custom props to MUI-Material ListItemButtonProps? Facing a TypeScript error?

This particular task should be relatively straightforward. I am seeking a custom component that inherits all props from ListItemButtonProps, while also adding an extra state prop: type SidebarListItemButtonProps = ListItemButtonProps & { state: Sideb ...

What steps should I take to fix this TypeScript error?

I've been struggling to fix this error, but I'm unsure of the best way to resolve it. Here is the code snippet I've been working with:https://i.sstatic.net/m6M48.png Here is the specific error message: https://i.sstatic.net/uk6QY.png ...

In TypeScript, how are angle brackets like methodName<string>() utilized?

Could someone please assist me in understanding why we use angular brackets <> in typescript? For example, I have provided some code below and would appreciate an explanation. export class HomePage { constructor(public navCtrl: NavController) ...

Mastering the proper implementation of the factory method in TypeScript

Trying to articulate this question is proving to be a challenge, but I'll give it my best shot. In Sharepoint, a default ListItem comes with properties like id, title, createdby, createddate, modifiedby, and modifieddate. Custom lists can have addit ...

The bespoke node package does not have an available export titled

No matter what I do, nothing seems to be effective. I have successfully developed and launched the following module: Index.ts : import ContentIOService from "./IOServices/ContentIOService"; export = { ContentIOService: ContentIOService, } ...

The build script does not execute during the creation of a Node.js and React application in Visual Studio

I am currently following a detailed guide on setting up Visual Studio 2019 to develop a Node.js-React application. The link to the guide can be found here: https://learn.microsoft.com/en-us/visualstudio/javascript/tutorial-nodejs-with-react-and-jsx?view=vs ...

"Enhancing user experience with MaterialUI Rating feature combined with TextField bordered outline for effortless input

I'm currently working on developing a custom Rating component that features a border with a label similar to the outlined border of a TextField. I came across some helpful insights in this and this questions, which suggest using a TextField along with ...

Creating a popup trigger in ReactJS to activate when the browser tab is closed

I am currently working on an enrollment form that requires customer information. If a user fills out half of the form and then attempts to close the tab, I want to trigger a popup giving them the option to save and exit or simply exit. While I have a jQue ...

Issue with ESLint error in TypeScript PrimeReact async Button click handler

I am currently facing an issue with exporting data from a DataTable in PrimeReact. The onClick function for the Button does not allow async callbacks as flagged by eslint. Can someone guide me on how to properly call this function? const exportCSV = us ...

"Unfortunately, Azure Web Static Apps do not have the capability to access .env files that are prefixed with NEXT

Suppose I have an .env file set up like this: NEXT_PUBLIC_API_BASE_PATH = value1 While this .env is functioning correctly in my local environment, once deployed to Azure Web Static Apps and added to the configurationhttps://i.sstatic.net/eqiYn.png My app ...

Unable to modify data with ionic and firebase in child node format

I am encountering an issue when updating data with Ionic Firebase using the following code. Instead of rewriting the previous data, it simply creates new data entries. Here is the code snippet: updateLaporan() { this.id =this.fire.auth.cur ...

Managing two subscriptions based on conditions in Angular

I'm currently working on a component that includes the following code snippet: this.service().subscribe((result) => { this.service2(result).subscribe((result2) => //other code }} As I strive to follow best practices in Angular development, I&ap ...

How can one properly conduct a health check on a Twilio connection using TypeScript?

How can I create an endpoint in TypeScript to verify if the Twilio connection is properly established? What would be the proper method to perform this check? Below is a snippet of my current code: private twilioClient: Twilio; ... async checkTwilio() { ...

What steps should I follow to properly set up my tsconfig.json in order to ensure that only the essential files are included when executing npm run build

Introduction I am seeking guidance on how to correctly set up my tsconfig.json file to ensure only the necessary files are included when running npm run build for my projects. I want to avoid any unnecessary files being imported. Query What steps should ...

The redirection code is not being executed when calling .pipe() before .subscribe()

My AuthService has the following methods: signUp = (data: SignUp): Observable<AuthResponseData> => { const endpoint = `${env.authBaseUrl}:signUp?key=${env.firebaseKey}`; return this._signInOrSignUp(endpoint, data); }; signIn = (data: SignIn): ...