Typescript dynamically define type for item in array which is in another object

Viewed 69

Here it is example on playground. Playground

interface Product {
  name: string,
  id: number
  productList?:ProductItem[]
}

interface ProductItem { 
  color: string, 
  size: number 
 }


type IValidation<T> = {
  field: keyof T
  nestedValidations?: IValidation<
    Pick<
      T,
      {
        [K in keyof T]-?: T[K] extends object ? K : never
      }[keyof T]
      >
    >[] // THIS IS IMPORTANT FOR QUESTION!
  validators?: (any | any | any)[]
}

export async function validateIt<T>(payload: T, validations: IValidation<T>[]): Promise<
  Partial<{
    [key in keyof T]: string[]
  }>
  > { 
    return Promise.resolve(payload);
  }

const product: Product = {
  id: 1,
  name: 'playstation',
  productList: [{
    color: 'red',
    size: 32
    }
  ]
}

const test = validateIt<Product>(product, [
  {
    field: "productList",
    validators: [],
    nestedValidations: [
      {
        field: 'color',
        validators: []
      }
    ]
  }
])

So this part gives an error Error

So clearly something is wrong with this part.

nestedValidations?: IValidation<
        Pick<
          T,
          {
            [K in keyof T]-?: T[K] extends object ? K : never
          }[keyof T]
          >
        >[]

Any idea how to write type for this property which is array, i have tried many things so i am trying basically to use same type for payload and validator. And to reuse same type for nested property, which is basically element in array. So overall trying to use same type IValidation.

productList?:ProductItem[]
1 Answers

Anyways, first things first, we have to change the Product interface instead to a type. Why?, the following changes I made will only work with type, I'm not really sure myself to why this is, but it fixes it. (No longer necessarily true, see edit at very bottom).

Anyways here's my answer, w/ explanation below. You only included one an example w/ array, so this only supports arrays to nestedValidate through.

type ProductNew = {
  name: string,
  id: number
  productList?:ProductItem[]
}

type IValidationNew<T> = {
  field: keyof T
  nestedValidations?: 
    (T extends Record<any, infer Value> 
    ? Value extends Array<infer Item> 
      ? Item extends Record<any, any> 
        ? IValidationNew<Item>
        : never
      : never
    : never
    )[]
  validators?: (any | any | any)[]
}

We do several important things here, namely these two lines

T extends Record<string, infer Value>
? Value extends Array<infer Item>
//...

The reason we must use Record to infer the value rather than use say, T[keyof T] to get some value, is because they discriminate types differently, T[keyof T] will give us a union of all possible values, whereas this will map these discriminately to their own properties. A demo will showcase this best: TS Playground

If you want to instead, here is one that works with both arrays and object keys.

type IValidationNew<T> = {
  field: keyof T
  nestedValidations?: 
    (T extends Record<any, infer Value> 
      ? Value extends Array<infer Item> 
        ? Item extends Record<any, any> 
          ? IValidationNew<Item>
          : never
      : Value extends Record<string, any>
        ? IValidationNew<Value>
        : never
    : never
    )
  validators?: (any | any | any)[]
}[]

TS Playground

There is a limitation though. We will only be able to typecheck deeper downwards but not upwards, so all your keys will be at the right depth, but we will be able to mix/match in the incorrect spots at times. I haven't been able to find any fix for this. This is because it is impossible to seperate the keys and their values. (See fix below)

const wackyProduct = {
  id: 1,
  name: 'playstation',
  productList: [{
    color: 'red',
    size: 32
    }
  ],
  otherValidationProps: {
    validateHere: {
      validateDeeper: 'string'
    }
  }
}
const test2 = validateIt(wackyProduct, [
  {
    field: "productList",
    validators: [],
    nestedValidations: [
      {
        field: 'validateHere', //OOPS how'd we do that?
        validators: []
      }
    ]
  },
  {
    field: 'otherValidationProps',
    nestedValidations: [
      {
        field: 'validateHere',
        validators: [],
        nestedValidations: [
          {
            field: 'validateDeeper',
            validators: [],
            nestedValidations: [ //Oops! No more keys any deeper
              {
                field: 'nothing'
              }
            ]
          }
        ]
      }
    ]
  }
])

EDIT Discussion 3/28/22

This should require all the keys in the correct places at the correct depths this time.

This is less verbose, and will require all the correct properties.

TS playground

type IValidation<T> = 
  T extends Array<infer R> 
  ? IValidation<R> 
  : T extends object 
  ? { [K in keyof T]: 
      { 
        field: K;
        nestedValidations?: IValidation<T[K]>
        validators?: any[] 
      } 
    }[keyof T][]
  : never
Related