简体   繁体   中英

Typescript nested type mapping

I'm trying to handle model validation in typescript. I'd like to be able to capture the nested types of the validation definition.

For example, I want to be able to create the validator like this.

const validateUser = createValidator({
  name: {
    first: {
      value: "First"
    },
    last: {
      value: "Last"
    }
  },
  age: {
    value: 32
  },
  hasOnboarded: {
    value: false
  }
});

This would create a validateUser function that takes a model of the specified type and validates its types.

I'd like to be able to capture the type so that validateUser knows to accept, objects that conform to the interface.

type ValidateUser = typeof validateUser;

Should be type

(
  model: {
    name: {
      first: string,
      last: string
    },
    age: number,
    hasOnboarded: boolean
  }
) => boolean

Is this possible in TypeScript?

It is, with the use of 2.8's features, conditional types and the infer keyword.

Playground

declare function createValidator<
  T extends {
    [key: string]: { value: any } | { [key: string]: { value: any } }
  }>(modelDescriptor: T): (validationSubject: {
    [key in keyof T]: T[key] extends { value: infer R }
    ? R
    : {
      [innerKey in keyof T[key]]: T[key][innerKey] extends { value: infer R } ? R : never
    }
  }) => boolean;

// const validateUser: (validationSubject: { name: { first: string; last: string; }; age: number; hasOnboarded: boolean; }) => boolean

const validateUser = createValidator({
  name: {
    first: {
      value: "First"
    },
    last: {
      value: "Last"
    }
  },
  age: {
    value: 32
  },
  hasOnboarded: {
    value: false
  }
});

Let's break it down, since this is a bit complicated.

First, our function accepts a type parameter T , with the constraint that it must be an object, where every key must be either a {value: any} or a nested object where every key is such a {value: any} .

Our return type is an object, such that for every key in T, we examine the type. If it is of type {value: <whatever>} , infer the <whatever> (and call it R ), the type of that key's value is R (aka whatever type the original had under the value key).

If it isn't of type {value: <whatever>} , then by our original constraint it must be of type {[key: string]: {value: <whatever>}} , so the return type becomes a second level mapping over that inner object, again extracting the type of whatever is in the value of the nested object.

If it is neither (which is not possible, thanks to our constraint), the return type for the key will be never . That part should never be reached.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM