Typescript - Identifying child type based on the specified property in keyof

I'm exploring the possibility of enhancing an interface by adding extra options to it. For example:

interface IRole {
  id: number;
  name: string;
}

interface IAddress {
  line1: string;
  line2: string;
  city: string;
  state: string;
  zip: string;
}

interface IUser {
  email: string;
  password: string;

  role?: IRole;
  addresses: IAddress[];
}

const options: FieldOptions<IUser> = {
  name: 'role',
  description: 'This is a role',
  children: [
    {
      name: 'name', // The child element declaration should be the type that is specified by the name
                    // In this case it should be FieldOptions<IRole>[]
                    // But right now it is FieldOptions<IRole | IAddress[] | string>[]
                    // I just need a way to narrow down to what it should be
      description: 'This is the name on the role'
    }
  ]
}

Currently, my FieldOptions interface looks like this, which I know is incorrect.

interface FieldOptions<T, K extends keyof T> {
  name: K;
  description?: string;
  children?: FieldOptions<T[K], keyof T[K]>[];
}

I've been searching for examples similar to mine, but most involve known keys and conditional properties. My scenario is different as it relies on unknowns until input from the user provides a type. Any insights or assistance would be greatly appreciated. Thank you!

Answer №1

I believe a type alias would suit your needs better than an interface in this case. The FieldOptions<IUser> should be represented as a union of structures corresponding to each choice for the name field, which interfaces cannot achieve.

Below is a recursive conditional mapped type that aims to express what you're trying to achieve:

type FieldOptions<T> = T extends object ? { [K in keyof T]-?: {
  name: K;
  description?: string;
  children?: FieldOptions<T[K]>[];
} }[keyof T] : never;

This code iterates over the property keys of an object type T and outputs a structure similar to the one you described earlier. This approach should work for subproperties as well due to the recursive nature of FieldOptions<T>.

If T is not an object type, it returns never, ensuring that children will not have any entries. You may need to make adjustments if T is an array or primitive type.

You can test it with some examples like the ones provided below:

const options: FieldOptions<IUser> = {
  name: 'role',
  description: 'This is a role',
  children: [
    {
      name: 'name',
      description: 'This is the name on the role',
      children: []
    }
  ]
}; // should be valid

const badOptions: FieldOptions<IUser> = {
  name: "role",
  children: [{
    name: "oops" // error!
  //~~~~ <-- incorrect name
  }]
}

The concept works best for plain object types but might require adjustments for arrays or primitives. Consider scenarios like FieldOptions<IAddresses[]> where you may need to handle properties differently (e.g., what should the name property be for numbers?).

Playground link to review code

Answer №2

@jcalz beat me to the post, but I have come up with something very similar.

type FieldOptions<T> = {
  [K in keyof T]: {
    name: K;
    description: string;
    children?: Required<T>[K] extends object ? FieldOptions<Required<T>[K]> : never;
  }
}[keyof T][]

The challenging aspect is using Required<T>[K] instead of just T[K]. The reason for this is because IUser['role'] results in IRole | undefined due to the property being optional. However, we aim to obtain the options for IRole while excluding the interference of undefined.

type UserOptions = FieldOptions<IUser>

This leads to:

type UserOptions = ({
    name: "email";
    description: string;
    children?: undefined;
} | {
    name: "password";
    description: string;
    children?: undefined;
} | {
    name: "role";
    description: string;
    children?: FieldOptions<IRole> | undefined;
} | {
    ...;
} | undefined)[]

I assume that your options were intended to be an array containing that object, as it wouldn't make much sense otherwise. This setup successfully validates in TypeScript using my type FieldOptions<IUser>.

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

The Ionic Image breaks upon publication and installation of the apk

Upon accessing my images using this method, I discovered that they are broken after being installed on an actual device. <ion-avatar item-left> <img src="../../assets/uploads/user.jpg"> </ion-avatar> <h2>{{user?.fullname}}</ ...

Inform the Angular2 Directive about any modifications

I'm facing an issue that needs solving. I have a component containing a template. ul(slider) li(*ngFor="let car of getRecentCars()") car-poster(bcg="{{car.recentBackgroundUrl}}", image="{{car.indexImage}}") Additionally, there is a slid ...

What is the process for extracting Excel .xlsx information from a POST request body in an Express API?

I've created an Angular frontend application that sends an excel (.xlsx) file as form data in the request body to my Express endpoint. Take a look at the function from my Angular service file below: uploadOrder(workOrder: File) { const formData: For ...

What is the method to incorporate type hints for my unique global Vue directives?

When I import or create a directive in an SFC file, I can receive TypeScript hints like this: import vFocus from 'my-custom-component-library'; View demo with ts hints However, if I globally register it in main.ts, the type hints are not availa ...

What is the best approach to implement custom JSX alongside React framework?

We have developed our own unique JSX syntax. For the purpose of integrating with JSX syntax highlighting tools, we have designated our file extension as .x.tsx While we created our own loader, using our JSX syntax alongside React may result in several err ...

Error in Angular 4: Undefined property 'replace' causing trouble

I've been trying to use the .replace() JavaScript function in Angular 4 to remove certain characters from a string. Here is the code snippet from my component: @Component({...}) export class SomeComponent implements OnInit { routerUrl: string = &apo ...

When defining a GraphQL Object type in NestJS, an error was encountered: "The schema must have unique type names, but there are multiple types named 'Address'."

Utilizing Nestjs and GraphQL for backend development, encountered an error when defining a model class (code first): Schema must contain uniquely named types but contains multiple types named "Address". Below is the Reader model file example: @ObjectType() ...

Looking for a button that can be toggled on and off depending on the input fields

Even after adding useEffect to my code, the button component remains disabled unless the input fields are filled. It never enables even after that. export default function Page() { const [newPassword, setNewPassword] = useState(''); const [conf ...

A solution to the error message "Type 'unknown' is not assignable to type 'Country[]' in React TypeScript" is to explicitly define the type of the

I encountered error TS2322: Type 'unknown' is not assignable to type 'Country[]' https://i.sstatic.net/O4gUu.png pages/Countries/index.tsx Full code: import * as ApiCountries from '../../services/APIs/countries.api' functi ...

Tips for creating an API URL request with two search terms using Angular and TypeScript

I have developed a MapQuest API application that includes two input boxes - one for the "from" location and another for the "to" location for navigation. Currently, I have hardcoded the values for these locations in my app.component file, which retrieves t ...

Organized modules within an NPM package

I am looking to develop an NPM package that organizes imports into modules for better organization. Currently, when I integrate my NPM package into other projects, the import statement looks like this: import { myFunction1, myFunction2 } from 'my-pac ...

List the attributes of combined interface declaration

I've come across a challenge when trying to extend the interfaces of third-party libraries globally. Whenever I import other files at the root level, the declaration file loses its global nature. Let me illustrate this with an example: Suppose I wan ...

Guide to setting up form data with asynchronous data

Greetings, experts! I have developed a web service using React. Now, I am looking to create a page for modifying user information. Although I can successfully receive user data and set it as the value of inputs, I am encountering a warning in React. ...

Combining union types with partial types in TypeScript: A guide

Consider the following type in TypeScript: type Input = { a: string b: number } | { c: string } How can we merge it into a partial type like this: type Result = { a?: string b?: number c?: string } We are seeking a type Blend<T>: type B ...

Building classes in TypeScript

There is a C# class called Envelope which contains properties for Errors, Paging, and Result. It also has multiple constructors to initialize these properties in different ways. export class Envelope<T> { errors: Error[]; paging: Paging; resu ...

How Can I Build a Dynamic Field Form Builder in Angular 4?

While working with dynamic JSON data, I needed to create fields dynamically. For instance, if my JSON array contains 3 values, I would generate 3 input checkboxes dynamically as shown below: <ng-template ngFor let-numberOfRow [ngForOf]="numberOfRows"&g ...

Implementing dynamic image insertion on click using a knockout observable

I'm utilizing an API to retrieve images, and I need it to initially load 10 images. When a button is clicked, it should add another 10 images. This is how I set it up: Defining the observable for the image amount: public imageAmount: KnockoutObserva ...

Retrieve the implementation of an interface method directly from the constructor of the class that implements it

I am looking to create a function that takes a string and another function as arguments and returns a string: interface Foo { ConditionalColor(color: string, condition: (arg: any) => boolean): string; } I attempted to pass the ConditionalColor metho ...

Creating formGroups dynamically for each object in an array and then updating the values with the object data

What I am aiming to accomplish: My goal is to dynamically generate a new formGroup for each recipe received from the backend (stored in this.selectedRecipe.ingredients) and then update the value of each formControl within the newly created formGroup with t ...

Metronome in TypeScript

I am currently working on developing a metronome using Typescript within the Angular 2 framework. Many thanks to @Nitzan-Tomer for assisting me with the foundational concepts, as discussed in this Stack Overflow post: Typescript Loop with Delay. My curren ...