c01nd01r
c01nd01r

Reputation: 617

Infer generic types for fields in nested object

I'm trying to make a data object for the state of a form with nested fields, but I'm having problems with type inference for nested fields. I've tried using unions and conditional types, but I don't fully understand how to do it correctly.

Link to TS Playground: https://tsplay.dev/mxoJzN

interface IField<V> {
  value: V;
}

class Field<V> implements IField<V> {
  value: V;

  constructor(params: { value: V }) {
    this.value = params.value;
  }
}


type TFields<T> = { [K in keyof T]: T[K] extends IField<T[K]> ? IField<T[K]> : TFields<T[K]> }

class Form<T> {
  fields: TFields<T>;
  
  constructor(fields: TFields<T>) {
    this.fields = fields;
  }
}

/*
Expected:
{
  id: IField<string>,
  auth: {
    login: IField<string>,
    password: IField<number>,
    code: {
      isValid: IField<boolean>
    }
  }
}

Received:
{
  id: string,
  auth: TFields<{
    login: string,
    password: number,
    code: TFields<{
      isValid: boolean
    }>
  }>
}
*/

type FormSchema = { 
  id: string,
  auth: {
    login: string,
    password: number,
    code: {
      isValid: boolean
    }
  }
}

const form = new Form<FormSchema>({
  id: new Field({ value: '' }),
  auth: {
    login: new Field({ value: '' }),
    password: new Field({ value: 0 }),
    code: {
      isValid: new Field({ value: false })
    }
  },
})

Upvotes: 1

Views: 384

Answers (1)

Oblosys
Oblosys

Reputation: 15136

If you want to recursively apply TFields for object properties, and use IField for the other properties, you can define TFields as

type TFields<T> = {
  [K in keyof T]: T[K] extends object ? TFields<T[K]> : IField<T[K]>
}

This will make the form sample correctly typed. If you have a specific set of primitive types, e.g. number, string, & boolean, you could also replace the IField<T[K]> above with T[K] extends number | string | boolean ? IField<T[K]> : never, so you'll get an error for unsupported types.

TypeScript playground

Upvotes: 1

Related Questions