2

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?

1 Answer 1

3

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 (a.k.a. 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.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks that works great. It only works 2 levels deep though. Do you think it would be possible to do this recursively, to allow an infinite depth?
@Cytren That would make TypeScript a turing complete typesystem, which it is not. I'm afraid it's not possible to do recursively.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.