Create a universal formatter for pairs of property names and values that can be applied to any property

Imagine we have an object definition as follows:

type Obj = {
    i: number
    b: boolean
    s: string
}

We want to create a type that allows us to pass a property name and a callback function that expects an argument of the same type as the specified property:

const a: FormatEntry = {
    accessor: 'b',
    formatter: (value: boolean) => value ? 'a' : 'b'
}

Here's what we have tried so far:

type FormatEntry<K extends keyof Obj> = {
    accessor: K,
    formatter: (value: Obj[K]) => string
}

However, this gives us an error message "Generic type 'FormatEntry' requires 1 type argument(s).(2314)" which is confusing because we expect TypeScript to infer the generic argument automatically.

We can manually provide the argument like this:

const a: FormatEntry<'b'> = {
    accessor: 'b',
    formatter: (value: boolean) => value ? 'a' : 'b'
}

But it seems redundant. What could be the issue here?

Answer №1

Regrettably, TypeScript does not automatically deduce type arguments for generic types such as your FormatEntry. Type arguments are only inferred when you invoke generic functions in TypeScript. This limitation has been acknowledged as a missing feature and has been requested to be implemented (microsoft/TypeScript#32794). If this feature were to be added, you could potentially write:

// NOT VALID TYPESCRIPT, DO NOT TRY THIS
type FormatEntry<K extends keyof Obj = infer> = {
    accessor: K,
    formatter: (value: Obj[K]) => string
}
const s: FormatEntry = { accessor: 's', formatter: s => s.toUpperCase() }
//    ^? const s: FormatEntry<"s">

This hypothetical code uses the default keyword "infer" in TypeScript's type parameter declaration and demonstrates how automatic inference would work if supported by the language. Until then, you will need to find alternative solutions.


One workaround is to use a generic helper function that mimics the behavior of your desired type:

const formatEntry = <K extends keyof Obj>(f: FormatEntry<K>) => f;

Instead of directly assigning values like const s: FormatEntry = {⋯}, you can now assign using the helper function like this: const s = formatEntry({⋯}):

const s = formatEntry({ accessor: 's', formatter: s => s.toUpperCase() });
//    ^? const s: FormatEntry<"s">

This approach enables TypeScript to properly infer the type argument K based on the input to the formatEntry() function, resulting in the correct type assignment for the variable s.


In the context of the provided example, you may not necessarily require FormatEntry to be generic at all. It appears that conceptually, FormatEntry could be represented as a union of its possible generic versions. Essentially, you might want a type equivalent to

FormatEntry<"i"> | FormatEntry<"b"> | FormatEntry<"s">
:

type FormatEntry = {
    accessor: "i";
    formatter: (value: number) => string;
} | {
    accessor: "b";
    formatter: (value: boolean) => string;
} | {
    accessor: "s";
    formatter: (value: string) => string;
}

This setup creates a discriminated union where accessor acts as the discriminant property. By leveraging this structure, TypeScript can correctly infer the parameter type of formatter based on the value of the accessor property.

To streamline the creation of this union type without manual intervention, we can compute the desired type based on the existing object Obj:

type FormatEntry = { [K in keyof Obj]: {
    accessor: K,
    formatter: (value: Obj[K]) => string
} }[keyof Obj];

This technique utilizes a distributive object type pattern, allowing TypeScript to derive the union type FormatEntry from the original definition of FormatEntry<K> for each key K in keyof Obj, thus eliminating the need for manual type declarations.

Link to TypeScript Playground with Code Example

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

What are the steps to customize the date pipe in Angular?

Encountered the InvalidPipeArgument issue with Safari for date/time format, but managed to resolve it by following the solution provided in this Stack Overflow thread. Currently using: <mat-cell *cdkCellDef="let payroll" fxFlex="20%"> {{payroll.tim ...

Instructions on how to deactivate or unselect a toggle switch by simply clicking on the icon among several icons

In my angular application, there are several icons with a toggle switch on the right side. The default state of the switch is ON. When an icon is clicked, its color changes from white to red. By clicking on the switch, the colored icon reverts back to whit ...

Unable to showcase the content inside the input box

I am having trouble displaying a default value in an input field. Here is how I attempted to do it: <input matInput formControlName="name" value="Ray"> Unfortunately, the value is not appearing as expected. You can view my code o ...

What could be the possible reason for the token having a null value during login authentication in

As a beginner to Angular, I am facing an issue with my JWT login page implementation. Despite printing the token in the console and confirming its existence as a string, I am receiving a null (or undefined) value. This is the code snippet from my UserServi ...

What distinguishes between the methods of detecting falsy and truthy values?

While working with JavaScript / Typescript, I often find myself needing to verify if a length exists or if a value is true or false. So, the main query arises: are there any differences in performance or behavior when checking like this... const data = [ ...

What is the best way to run a scheduled task automatically using node-cron?

I have a custom function that saves data to the database using the POST method. When testing it with Postman, I send an empty POST request to trigger the controller. Upon execution, the function triggers two more functions. One of them is responsible for ...

Creating a key-constrained type in Typescript for object literals with automatically deduced number values

Suppose we have an object literal defined as: export const SOURCE = { UNKNOWN: 'Unknown', WEB: 'Web', MOBILE: 'Mobile', ... } as const; and export const OTHER_SOURCE = { UNKNOWN: 0, WEB: 1, MOBILE: ...

Utilizing Dependency Injection with Angular 2, Ionic 2, and TypeScript

I'm currently facing issues with bootstrapping certain files in my application. Specifically, I am working on extending the Ionic2 tabs example. Within my codebase, I have a Service and a User file, both annotated with @injectable. I am aiming for a ...

Compilation issues encountered while running TypeScript in Dockerfile

I am encountering an issue with my Dockerfile during the TypeScript compilation step. Below is the snippet of code where it fails: FROM node:12 WORKDIR /usr/src/app # SETUP COPY package.json . COPY tsconfig.json . COPY src . RUN npm install -g yarn@^1. ...

struggling with typing an object in react typescript - Element is assumed to have an 'any' type due to its type

Having trouble with the code snippet below: // if I remove the `any` below it breaks. const teams: any = { liverpool: <Liverpool />, manUtd: <ManUtd />, arsenal: <Arsenal />, }; export const TeamCrest = ({ team }: { team: ke ...

Can you explain the significance of the colon in this context?

Upon reviewing some SearchKit code snippets (composed with react/jsx and es2015), I came across the following line in a jsx file: const source:any = _.extend({}, result._source, result.highlight) I am curious about the purpose or significance of the colo ...

The module for the class could not be identified during the ng build process when using the --

Encountering an error when running: ng build --prod However, ng build works without any issues. Despite searching for solutions on Stack Overflow, none of them resolved the problem. Error: ng build --prod Cannot determine the module for class X! ...

Guide to eliminating the backslash character from a string in TypeScript (Angular)

I encountered an issue with a string: "\YES\" What is the best way to strip out the backslashes from this string? sessionStorage.getItem('questionList2')!.replace(/"\"/g,'') Unfortunately, running th ...

Utilizing React with .NET 8.0 and Minimal API, the front end is configured to send HTTP requests to its own specific port rather than the

Transitioning from working with react projects on .NET 3.1 to creating a new project on .NET 8.0 has been a challenging and confusing experience for me. When I attempted to access a newly created controller method, I encountered the error 404 Not Found. It ...

What are the top tips for creating nested Express.js Queries effectively?

I'm currently exploring Express.js and tackling my initial endpoint creation to manage user registration. The first step involves verifying if the provided username or email address is already in use. After some investigation, I devised the following ...

Encountered Error: Unknown property 'createStringLiteral', causing TypeError in the Broken Storyshots. This issue is happening while using version ^8.3.0 of jest-preset-angular

When I upgraded to jest-angular-preset ^8.3.0 and tried to execute structural testing with npm run, I encountered the following error: ● Test suite failed to run TypeError: Cannot read property 'createStringLiteral' of undefined at Obje ...

Explore the titles provided by Wikipedia

Hi there, I'm fairly new to Angular and trying to work with the Wikipedia API. However, I seem to be having trouble getting 4 titles from the API. Here's an example URL for one of the titles: https://en.wikipedia.org/w/api.php?action=query&pr ...

Instead of leaving an Enum value as undefined, using a NONE value provides a more explicit

I've noticed this pattern in code a few times and it's got me thinking. When you check for undefined in a typescript enum, it can lead to unexpected behavior like the example below. enum DoSomething { VALUE1, VALUE2, VALUE3, } f ...

The function plainToClass does not transform a Date object into a string

Based on the information provided in the documentation, it is recommended that a Date object be converted to a string: It's important to note that dates will automatically be converted to strings when attempting to convert a class object to a plain ...

Error: Bootstrap CSS missing from app created with create-react-app-ts

I have a React application that was initially set up using create-react-app-ts. Although I have included bootstrap and react-bootstrap for navigation, the CSS styling from Bootstrap does not seem to be applied properly. The links do not display any styling ...