2

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[]
Ambulance lada
  • 311
  • 1
  • 2
  • 14

1 Answers1

0

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
Cody Duong
  • 2,292
  • 4
  • 18
  • tnx but i am trying to find more elegant solution with types over interfaces – Ambulance lada Mar 25 '22 at 08:41
  • @Ambulancelada If you mean using a type to map over the properties of an interface, it is simply not possible. This is because interface is already a runtime feature, and does not have full typing support, this includes but is not limited to over remapping property key/value pairs. – Cody Duong Mar 25 '22 at 20:45
  • what about something like this type IValidation = T extends Array ? IValidation : T extends object ? { [K in keyof T]: { field: K nestedValidations?: IValidation[] validators?: (IRequiredValidator | IEnumValidator | IFileValidator | IIntegerValidator)[] } }[keyof T] : never – Ambulance lada Mar 28 '22 at 10:21
  • With a little modification that is the right idea it will work, I think this should work for your use case. this actually has some unique type inferencing I didn't think of originally, and will add to my original answer. See the EDIT for the new answer. – Cody Duong Mar 29 '22 at 04:06
  • this will work in 3.7 ts only – Ambulance lada Mar 29 '22 at 08:58
  • Is this an issue? If so I would recommend upgrading to TS^3.7, otherwise I'm not really aware of any workaround for this. Interfaces are recursive but cant be used with conditional types or mapped types. Any workaround would simply be harder, and less elegant to implement rather than upgrading. Esp, since 3.0 has been out for 4 years, you should be able to upgrade with no breaking changes. – Cody Duong Mar 29 '22 at 16:49