0

I do have the following code:

type DomainFieldDefinition<T> = {
  required?: boolean
}

type DomainDefinition<F, M> = {
  fields?: { [K in keyof F]: DomainFieldDefinition<F[K]> },
  methods?: { [K in keyof M]: M[K] & Function },
}

type User = {
  id: string,
  name?: string
}

export const User = createDomain<User>({
  fields: {
    id: { required: true },
    name: {},
  },
});

I am trying to validate that the required key in the definition object passed to the createDomain method for a field has a value that matches the requiredness of the type it is based on (here, User) ; ideally at compile time.

I have the sensation that conditional types may help in doing so, but I could not find a way to do so based on requiredness. Specifically, I'm trying to constrain required to be:

  • true if the field is not nullable,
  • false or undefined if it is

Any hints?

Pierre
  • 6,084
  • 5
  • 32
  • 52

1 Answers1

1

Using the types defined here as an example we can create a conditional type where if the field is required field type will be of type { required : true } or {} otherwise:

type DomainDefinition<F, M> = {
    fields?: {
        [K in keyof F]: ({} extends { [P in K]: F[K] } ? {} : { required: true }) & {} // Intersect with other properties as necessary
    },
    methods?: { [K in keyof M]: M[K] & Function },
}

type User = {
    id: string,
    name?: string
}

function createDomain<T>(o: DomainDefinition<T, any>) {
    return o;
}

export const User = createDomain<User>({
    fields: {
        id: { required: true },
        name: {},
    },
});

Note This will test for optionality (the ? modifier) it will not test for nullability ( | null | undefined) depending on your use case this may or may not be important.

Also of interest may be this answer that has a test for the readonly modifier. Using it, you can also add a isReadonly field:

type IfEquals<X, Y, A, B> =
    (<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? A : B;

type DomainDefinition<F, M> = {
    fields?: {
        [K in keyof F]:
        ({} extends { [P in K]: F[P] } ? {} : { required: true })
        & IfEquals<{ [P in K]: F[P] }, { -readonly [P in K]: F[P] }, {}, { isReadonly: true }>
    },
    methods?: { [K in keyof M]: M[K] & Function },
}

type User = {
    id: string,
    readonly name?: string
}

function createDomain<T>(o: DomainDefinition<T, any>) {
    return o;
}

export const User = createDomain<User>({
    fields: {
        id: { required: true },
        name: { isReadonly: true },
    },
});

If you want to filter out some properties, for example functions, you would have to replace all occurrences of F with a filtered F. To make it simpler just define an extra type alias:

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type DomainPropertyHelper<F> = {
  [K in keyof F]: ({} extends { [P in K]: F[K] } ? {} : { required: true }) & {} // Intersect with other properties as necessary
}; 
type DomainDefinition<F, M> = {
    fields?: DomainPropertyHelper<Pick<F, NonFunctionPropertyNames<F>>>,
    methods?: { [K in keyof M]: M[K] & Function },
}
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • That is a great answer, thanks. I have one additional question though ; your proposal works as is. However, it fails whenever I try to replace `[K in keyof F]` by `[K in NonFunctionPropertyNames]`, with `NonFunctionPropertyNames` defined as on the page https://www.typescriptlang.org/docs/handbook/advanced-types.html. The conditional test is only negative with that change. Would you have any idea why, by luck? – Pierre Jan 16 '19 at 18:00
  • 1
    @Pierre added the code to make it work with filtering as well – Titian Cernicova-Dragomir Jan 16 '19 at 18:15