1

I want to create a type that will have a generic type and 2 properties that will split nested paths of specified type.

export type RecursiveKeyOf<TObj extends object> = {
    [TKey in keyof TObj & (string | number)]: TObj[TKey] extends any[]
    ? `${TKey}`
    : TObj[TKey] extends object
    ? `${TKey}` | `${TKey}.${RecursiveKeyOf<TObj[TKey]>}`
    : `${TKey}`;
}[keyof TObj & (string | number)];

type SubRecursiveKeys<RK extends string, PFX extends RK> = RK extends `${PFX}.${infer SubKey}` ? SubKey : never;

const a = {
    level1: {
        level2: {
            level3: {
                level4: {
                    level5: 'test'
                }
            }
        }

    }
}


type SubKeys = SubRecursiveKeys<RecursiveKeyOf<typeof a>, 'level1.level2'> // ok

// error here - does not satisfy the constraint 
type ShortHandKeyMapper<F extends object, Base extends RecursiveKeyOf<F> = RecursiveKeyOf<F>> = {
    control: Base                              // error here - does not satisfy the constraint 
    value: SubRecursiveKeys<RecursiveKeyOf<F>, Base>
}

const denyMapper: ShortHandKeyMapper<typeof a> = {
    control: 'level1.level2.level3',
    value: 'level2' // shouldnt allow this
}

const okMapper: ShortHandKeyMapper<typeof a> = {
    control: 'level1.level2.level3',
    value: 'level4' // should allow this or 'level4.level5'
}

ShortHandKeyMapper is the type that will have control as the prefix part of a nested path and value as the remaining part.

I get does not satisfy the constraint that makes no sense.

Playground

1 Answers1

0

The problem is in RecursiveKeyOf, it is written with mistakes. Using (string | number) here TKey in keyof TObj & (string | number) is not correct.

Please consider this example:

type RecursiveKeyOf<T, Cache extends string = ''> =
    T extends PropertyKey ? Cache : {
        [P in keyof T]:
        P extends string
        ? Cache extends ''
        ? RecursiveKeyOf<T[P], `${P}`>
        : Cache | RecursiveKeyOf<T[P], `${Cache}.${P}`>
        : never
    }[keyof T]

type SubRecursiveKeys<RK extends string, PFX extends RK> = RK extends `${PFX}.${infer SubKey}` ? SubKey : never;

const a = {
    level1: {
        level2: {
            level3: {
                level4: {
                    level5: 'test'
                }
            }
        }

    }
}

const infered = <Obj extends object,>(obj: Obj) =>
    <Control extends RecursiveKeyOf<Obj>>(
        control: Control,
        value: SubRecursiveKeys<RecursiveKeyOf<Obj>, Control>
    ) => ({
        control,
        value
    })

const curry = infered(a)

const _ = curry('level1.level2.level3', 'level4') // ok
const __ = curry('level1.level2.level3', 'level4.level5') // ok
const ___ = curry('level1.level2.level3', 'level5') // expected error

Playground

Please see related answers: here , here and here

Also, you can check my article

In order to have a guarantee that you object is valid, you need to infer control and then TS will be able to compute value.

If you don't need/want to use a function for inference, you can generate all allowed states of an object. See this:

type RecursiveKeyOf<T, Cache extends string = ''> =
    T extends PropertyKey ? Cache : {
        [P in keyof T]:
        P extends string
        ? Cache extends ''
        ? RecursiveKeyOf<T[P], `${P}`>
        : Cache | RecursiveKeyOf<T[P], `${Cache}.${P}`>
        : never
    }[keyof T]

type SubRecursiveKeys<RK extends string, PFX extends RK> = RK extends `${PFX}.${infer SubKey}` ? SubKey : never;

const a = {
    level1: {
        level2: {
            level3: {
                level4: {
                    level5: 'test'
                }
            }
        }

    }
}

type Values<T> = T[keyof T]

type ValidState<Obj> = Values<{
    [Prop in RecursiveKeyOf<Obj>]: {
        control: Prop,
        value: SubRecursiveKeys<RecursiveKeyOf<Obj>, Prop>
    }
}>

type Result = ValidState<typeof a>

// expected error
const denyMapper: Result = {
    control: 'level1.level2.level3',
    value: 'level2' // shouldnt allow this
}

// ok
const okMapper: Result = {
    control: 'level1.level2.level3',
    value: 'level4' // should allow this or 'level4.level5'
}

Playground

type Result is just a union of all allowed states