1

English is not my first language.

I'm trying to make an interface/type that verifies if the value passed is a variation of another interface.

Verify if the object that was valid with:

const obj1 = {Ab: {Cd: 1}};

Is also valid as:

const obj2 = {"Object.Ab.Cd": 1};

At the moment, the closest that I could get was this.

The return type is not that important right now, for now, my focus is on the properties.

Here is my code at the moment:

type RecursiveObjectFilter<T> = {
  [P in keyof T as `.${Capitalize<string & RecursiveObjectFilter<P>>}`]: RecursiveObjectFilter<P>;
};

type Validate<T> = T extends string | number | bigint | boolean ? T : RecursiveObjectFilter<T>;

export interface IFilterBack<T> {
  Object?: {
    [P in keyof T as `Object${Capitalize<string & Validate<P>>}`]?: Validate<P>;
  };
  PageNumber?: number;
  RowsPerPage?: number;
  OrderByColumn?: string;
}

interface test {
  Ab: { Cd: number };
}

const a: IFilterBack<test> = {
  Object: { "Object.Ab.Cd": 1 }
};
Richard Wilson
  • 297
  • 4
  • 17
Ravelli
  • 13
  • 3

1 Answers1

1

This task could be split into two less complex ones. First, to rename object keys prefixing them with full path to the property. And then deep flatten the object.

Renaming keys I believe is the most simple part here:

type RemapKeys<T, D extends number = 5, P extends string ="Object"> = 
    [D] extends [never] ? never : T extends object ? 
    { [K in keyof T as K extends string ? `${P}.${K}` : never]: RemapKeys<T[K], Prev[D], K extends string ? `${P}.${K}` : never> } : T

Here we just keep some prefix P and when meet any object call RemapKeys recursively with new prefix consisting of previous P value and the key of the object we're iterating over. We're using mapped types, key remapping and recursive contidional types here.

After renaming our new type has structure as follows:

interface test {
  Ab: { Cd: number, Ef: { Gh: string } };
  Ij: boolean;
}

/*
type Remapped = {
    "Object.Ab": {
        "Object.Ab.Cd": number;
        "Object.Ab.Ef": {
            "Object.Ab.Ef.Gh": string;
        };
    };
    "Object.Ij": boolean;
}
*/
type Remapped = RemapKeys<test>

Then comes the harder part. Flattening the object.

So the flattened object is the object that consists of the all the properties we have on the root level plus all the properties of the nested objects:

// root level properties
type NonObjectPropertiesOf<T> = {
  [K in keyof T as T[K] extends object ? never : K]: T[K]
}

// nested object values
type ValuesOf<T> = T[keyof T];
type ObjectValuesOf<T> = Extract<ValuesOf<T>, object>

But ObjectValuesOf gives us a union of objects' values. While we need an intersection. That's where the awesome UnionToIntersection type from @jcalz comes handy. So, for one-level nested object the Flatten type could be written as:

type Flatten<T> = NonObjectPropertiesOf<T> & UnionToIntersection<ObjectValuesOf<T>>

/*
type FlattenOneLevelRemapped = {
    "Object.Ij": boolean;
} & {
    "Object.Ab.Cd": number;
    "Object.Ab.Ef": {
        "Object.Ab.Ef.Gh": string;
    };
}
*/
type FlattenOneLevelRemapped = Flatten<Remapped>

But for deeply nested type we'll need recursion.

type DeepFlatten<T, D extends number = 5> = [D] extends [never] ? never : T extends unknown
  ? NonObjectPropertiesOf<T> &
      UnionToIntersection<DeepFlatten<ObjectValuesOf<T>, Prev[D]>>
  : never;

/*
type DeepFlattenRemapped = {
    "Object.Ij": boolean;
} & {
    "Object.Ab.Cd": number;
} & {
    "Object.Ab.Ef.Gh": string;
}
*/
type DeepFlattenRemapped = DeepFlatten<Remapped>

And finally combining it all together:

type IFilterBack<T> = {
  Object: DeepFlatten<RemapKeys<T>>
}

interface test {
  Ab: { Cd: number, Ef: { Gh: string } };
  Ij: boolean;
}

const a: IFilterBack<test> = {
  Object: { 
    "Object.Ab.Cd": 1, 
    "Object.Ij": true, 
    "Object.Ab.Ef.Gh": '',
  }
};

playground link


I didn't account for properties that can have array type here and not sure this type will scale good enough to fit them.

aleksxor
  • 7,535
  • 1
  • 22
  • 27
  • For now it seems to be working greatly. This is going to be used on a filter so i dont really think that arrays will be a problem, i will write here if i notice anything weird. And thank you for the detailed explanation. – Ravelli Jul 13 '21 at 12:41