What is the best way to define the type of an object in TypeScript when passing it to a function

I am currently working on a function that accepts an object of keys with values that have specific types. The type for one field is determined by the type of another field in the same object. Here is the code:

// Consider this Alpha type and echo function...

type NoInfer<T> = [T][T extends unknown ? 0 : never]

interface Alpha<Foo extends string> {
  foo: Foo
  bar: `Depends on ${NoInfer<Foo>}`
}

declare const echo: <T extends string>(x: Alpha<T>) => void

echo({ foo: 'beta', bar: 'Depends on beta'})

// @ts-expect-error Trailing 2 is wrong
echo({ foo: 'beta', bar: 'Depends on beta 2'})

// Now we need a function (bravo) that takes a keyed index of Alphas...

declare const bravo: <T extends { [k: string]: Alpha<string> }>(xs: T) => void

bravo({
  one:  { foo: `1`,  bar: `Depends on 1` },
  // @ts-expect-error 1 !== 1x           <-- fails
  oneX: { foo: `1x`, bar: `Depends on 1` },
  two:  { foo: `2`,  bar: `Depends on 2` },
  // @ts-expect-error 2 !== 2x           <-- fails
  twoX: { foo: `2x`, bar: `Depends on 2` },
})

// How can I make this work?

playground link

As indicated by the "fails" comments, while I can get Alpha to work initially, I encounter difficulties with more complex objects of Alphas. Can you assist me in resolving this issue? Thank you!

Answer №1

You have the option to structure this in a way where T serves as an object type with properties represented by the string values you provide as type arguments to Alpha. Then, you can turn xs into a mapped type over T, like so:

declare const bravo: <T extends { [K in keyof T]: string }>(
  xs: { [K in keyof T]: Alpha<T[K]> }
) => void

The recursive constraint { [K in keyof T]: string } is utilized to ensure that all properties of T are of type string, without relying on the index signature { [k: string]: string }, which would reject interface types lacking index signatures (refer to microsoft/TypeScript#15300 and How to constrain a TypeScript interface to have only string property values? for more details).

Since the type of xs is a homomorphic mapped type (as described in What does "homomorphic mapped type" mean?), the compiler can infer T from it when you invoke the function (although this was previously documented, the new handbook doesn't seem to mention it 🤷‍♂️). Let's put it to the test:

bravo({
  one: { foo: `1`, bar: `Depends on 1` },  // valid
  oneX: { foo: `1x`, bar: `Depends on 1` }, // error
  // --------------> ~~~
  // Type 'Depends on 1' is not compatible with type 'Depends on 1x'
  two: { foo: `2`, bar: `Depends on 2` }, // valid
  twoX: { foo: `2x`, bar: `Depends on 2` }, // error
  // --------------> ~~~
  // Type 'Depends on 2' is not compatible with type 'Depends on 2x'
})

All seems well. If you hover over the function call in an IDE with IntelliSense capabilities, you'll receive Quick Info

/* const bravo: <{
    one: "1";
    oneX: "1x";
    two: "2";
    twoX: "2x";
}>(xs: {
    one: Alpha<"1">;
    oneX: Alpha<"1x">;
    two: Alpha<"2">;
    twoX: Alpha<"2x">;
}) => void */

This shows that T is inferred as

{one: "1", oneX: "1x", two: "2", twoX: "2x"}
, leading to xs's type being compared against
{one: Alpha<"1">, oneX: Alpha<"1x">, two: Alpha<"2">, twoX: Alpha<"2x">}
, resulting in validation for the one and two properties but failing for the oneX and twoX properties, providing the desired errors.

Access the code via 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

Fresh React framework

I haven't worked on a React app in a while, but when I decided to start a new one and import my old function, I encountered the following error: C:/Users/Hello/Documents/Dev/contacts/client/src/App.tsx TypeScript error in C:/Users/Hello/Documents/Dev ...

Guide to integrating a component within another component

Due to the size of this application, I am unable to utilize app.module. Unfortunately, the component I need to implement does not contain a module file. I have attempted to import it and utilize @NgModule, but it has not been successful. An error occur ...

Ways to retrieve the name of the chosen option from a dropdown menu

Is there a way to retrieve the text name of a selected dropdown value using a PrimeNG dropdown? Incorporating a PrimeNG dropdown: HTML <p-dropdown [options]="regionSelectList" [(ngModel)]="reg" [filter]="true" [ngModelOptions]="{standalone: true}"> ...

Tips for effectively handling notifications using a single state management system

This React project showcases the Notification System demo provided by Mantine. It includes several function components utilizing notification. const A = () => { useEffect(() => { notifications.show({ // config }); }, []); retur ...

Displaying a random element from the state array in React Native

I'm attempting to display a random item from the state array, with the possibility of it changing each time the page reloads. Here's what I have tried so far, any suggestions or ideas are welcome! This is my current state: state = { randomIt ...

The Angular program is receiving significant data from the backend, causing the numbers to be presented in a disorganized manner within an

My Angular 7 application includes a service that fetches data from a WebApi utilizing EntityFramework to retrieve information. The issue arises when numeric fields with more than 18 digits are incorrectly displayed (e.g., the number 23434343434345353453,3 ...

Just a straightforward Minimum Working Example, encountering a TypeScript error TS2322 that states the object is not compatible with the type 'IntrinsicAttributes & Props & { children?: ReactNode; }'

Currently, I am immersed in a project involving React and Typescript. I am grappling with error code TS2322 and attempting to resolve it. Error: Type '{ submissionsArray: SubmissionProps[]; }' is not assignable to type 'IntrinsicAttributes ...

Unspecified properties emerge post-Angular update

We recently consolidated multiple Angular 16 projects into one NX mono repository using Angular 17. Everything is functioning properly, EXCEPT we have noticed a peculiar change in behavior with our models. Previously, unset properties were simply not displ ...

Typescript: Determining the return type of a function based on its input parameters

I've been working on a function that takes another function as a parameter. My goal is to return a generic interface based on the return type of the function passed in as a parameter. function doSomething <T>(values: Whatever[], getter: (whatev ...

When attempting to trigger a function by clicking a button in Angular 8 using HTTP POST, nothing is happening as

I've been struggling to send a POST request to the server with form data using Observables, promises, and xmlhttprequest in the latest Angular with Ionic. It's driving me crazy because either I call the function right at the start and the POST wo ...

How can TypeORM be used to query a ManyToMany relationship with a string array input in order to locate entities in which all specified strings must be present in the related entity's column?

In my application, I have a User entity that is related to a Profile entity in a OneToOne relationship, and the Profile entity has a ManyToMany relationship with a Category entity. // user.entity.ts @Entity() export class User { @PrimaryGeneratedColumn( ...

Iterating through typescript enums in Vue using v-for

Why is the v-for loop over an enum displaying both names and values? Is there a way to iterate only over the keys? export enum Colors { "RED" = 1, "BLUE" = 2, "GREEN" = 3, } <template> <div> <v ...

Is the Inline Partial<T> object still throwing errors about a missing field?

I recently updated to TypeScript version ~3.1.6 and defined an interface called Shop as follows: export interface Shop { readonly displayName: string; name: string; city: string; } In this interface, the property displayName is set by the backend a ...

Eliminate duplicated partial objects within a nested array of objects in TypeScript/JavaScript

I'm dealing with a nested array of objects structured like this: const nestedArray = [ [{ id: 1 }, { id: 2 }, { id: 3 }], [{ id: 1 }, { id: 2 }], [{ id: 4 }, { id: 5 }, { id: 6 }], ] In the case where objects with id 1 and 2 are already grou ...

How to remove a specific type from a generic type in Typescript without using Exclude<>?

I am looking for a solution to prevent my function from working with Moment objects when storing values in local storage. Currently, the function dynamically stringifies and stores values, but I want to exclude Moment objects from being processed. Here is ...

Troubleshooting the issue of process.nextTick not being recognized in Calgolia places.js

After successfully implementing Algolia's places.js in my Angular 7 project using NPM, I encountered an issue. I have integrated a form where one of the fields should be an input. <form [formGroup]="myForm"> <pre style="color: white; b ...

Discovering the process of reaching service members through an HTML View

Currently, I am in the process of learning Angular 2 and find myself unsure about the most efficient way to update the view. For instance, let's say I have two components: User and Main. The User component retrieves a list of users from the UserServ ...

No errors are being displayed with the React Hook Form using Zod and Material UI

Presenting my custom ProductInfoForm built using Material UI, react-hook-form with zod validations. However, I am encountering an issue: upon submitting the form, the data is displayed in the console as expected, but when intentionally triggering an error, ...

Loop through the information retrieved from the alertController

I'm currently facing an issue regarding accessing data in my alert controller let alert = this.alertCtrl.create({ title: 'Edit Index', inputs:this.customIndexes, buttons:[ { text: 'Cancel', role: 'cancel ...

Encountering a TypeScript issue with bracket notation in template literals

I am encountering an issue with my object named endpoints that contains various methods: const endpoints = { async getProfilePhoto(photoFile: File) { return await updateProfilePhotoTask.perform(photoFile); }, }; To access these methods, I am using ...