MacD
MacD

Reputation: 426

Recursive Type Mapping in Typescript

I'm trying to extract a type mapping from an object containing native type validation function so for example, if I have the object

const test  = {
  Test: {
    Test1: {
      Test2: SimpleStringValidator //return type is string or undefined but input can be anything
    },
  }
}

I want to extract the type

type Extracted = {
  Test: {
    Test1: {
      Test2: string
    }
  }
}

Towards this end I wrote the following code to extract the return types of an object containing a set of nested validation functions

Sample.ts


export type Validator<T> = NativeTypeValidator<T> | ObjectValidator<T>

export type NativeTypeValidator<T> = (n: any) => T | undefined
export type ObjectValidator<O> = {
  [K in keyof O]: Validator<O[K]> 
}

//native validators
export const SimpleStringValidator:NativeTypeValidator<string> = (val) => typeof(val) === "string" ? val : undefined

//object validator function
export const ObjValidator = <V>(validatorObj: ObjectValidator<V>) => (o:any):V =>{
  let result = {} as V;
  //we can only validate objects
  if (typeof (o) !== "object") { return undefined; }
  const validatorKeys = Object.keys(o) as [keyof ObjectValidator<V>]
  validatorKeys.forEach((validatorKey) => { 
    const objValue = o[validatorKey] as V[keyof V];
    const objectValidator = validatorObj[validatorKey]
    if (!objectValidator) { return undefined } //do nothing if no validator exists for the key in o
    //figure out if we have a nested object validator or a native validator at the corresponding key of validatorObj
    if (typeof (objectValidator) === "object") {
      result[validatorKey] = ObjValidator(objectValidator as ObjectValidator<V[keyof V]>)(objValue)
    }
    else {
      const nativeValidator = objectValidator as NativeTypeValidator<V[keyof V]>;
      result[validatorKey] = nativeValidator(objValue)
    }
  })
  return result;
}

export const test  = {
  Test: {
    Test1: {
      Test2: SimpleStringValidator
    },
  }
}

export const validatorFunc = ObjValidator(test);
export const outputExample = validatorFunc({
  Test: {
    Test1: {
      Test2: "hi"
    },
  }
})

outputExample.Test.Test1.Test2 = "1";
outputExample.Test.Test1.Test2 = 1; //vs code intellisense complains because needs to be type string

In my case intellisense auto-completes the nested property Test2 and has the type as string but when I create the type declaration files the type information is different and I can't export this generated type to other projects correctly. Specifically it sets the type of property Test1 to any. The type I'm looking to generate is the output of validatorFunc and the type of outputExample in the code above.

Generated sample.d.ts

export declare type Validator<T> = NativeTypeValidator<T> | ObjectValidator<T>;
export declare type NativeTypeValidator<T> = (n: any) => T | undefined;
export declare type ObjectValidator<O> = {
    [K in keyof O]: Validator<O[K]>;
};
export declare const SimpleStringValidator: NativeTypeValidator<string>;
export declare const ObjValidator: <V>(validatorObj: ObjectValidator<V>) => (o: any) => V;
export declare const test: {
    Test: {
        Test1: {
            Test2: NativeTypeValidator<string>;
        };
    };
};
export declare const validatorFunc: (o: any) => {
    Test: {
        Test1: any;
    };
};
export declare const outputExample: {
    Test: {
        Test1: any;
    };
};

I need to use the root validator object as the source for my extracted type looking for a solution where the generated declaration file has the correct type. I have this code up on Typescript playground and it shows the difference between autocomplete and what gets put into the .d.ts file.

TypescriptPlayground example

Upvotes: 0

Views: 3941

Answers (1)

Elias Schablowski
Elias Schablowski

Reputation: 2812

If you want to extract:

{
  Test: {
    Test1: {
      Test2: string
    }
  }
}

You can use this recursive type mapping, using infer:

type ValidatedObject<T> = Partial<{
  [key in keyof T]: T[key] extends ObjectValidator<infer Type>
    ? ValidatedObject<Type>
    : T[key] extends NativeTypeValidator<infer Type>
    ? Type
    : T[key] extends object
    ? ValidatedObject<T[key]>
    : T[key];
}>

Playground

Note: The partial is there for two reasons, one is that the type is outputted fully to the .d.ts file and the other is the undefined return value.

Upvotes: 2

Related Questions