Tobias Diez
Tobias Diez

Reputation: 271

Infer nested mapped type

I would like to use one member needs of an object to restrict the type of another compute member in the same type. So something like

class User {
    public name: string = ''
    public last: string = ''
}

extend({
    needs: {last: true}, // Select the members of User you want to have in compute
    compute: (user) => user.name // user is restricted to Pick<User, 'last'>, so this will give a ts error
})

This is relatively easy to archive. But a problem arises if you want to have a named collection of such declarations:

extend({
    lastName: {
        needs: {last: true},
        compute: (user) => user.last
    },
    myName: {
        needs: {last: name},
        compute: (user) => user.name
    }
})

Here is my shot at it, but typescript currently fails to properly infer the generic (see last "failing" declaration):

type ComputedField<T, K extends keyof T, V> = {
  needs: Record<K, boolean>
  compute: (input: Pick<T, K>) => V
}
type Narrowed<P, O extends Needs<P>> = {
    [key in keyof O]: O[key] extends {needs: infer K} ? keyof K : never
}
type NarrowedComp<P, O> = {
    [key in keyof O]: O[key] extends keyof P ? ComputedField<P, O[key], any> : never
}

type Needs<P> = {[key: string]: {needs: Partial<Record<keyof P, boolean>>}}

function extend<T extends Needs<User>>(input: NarrowedComp<User, Narrowed<User, T>>): T {return this}

// Some tests that this works as expected
const needExample = {
    fullName: {
        needs: {
            last: true
        }
    }
} satisfies Needs<User>
const test = {
    fullName: {
        needs: {last: true},
        compute: (user) => user.name // Expected error!
    }
} satisfies NarrowedComp<User, Narrowed<User, typeof needExample>>

const working = extend<typeof needExample>({ // works if we manually specify the generic
    fullName: {
        needs: {last: true},
        compute: (user) => user.name // Expected error!
    }
})

const failing = extend({ // but not if typescript should infer the generic
    fullName: {
        needs: {last: true}, // No error expected here, but got "Property 'name' is missing in type '{ last: true; }' but required in type 'Record<keyof User, boolean>'."
        compute: (user) => user.name // Expect error here, but none is shown!
    }
})
// So typescript infers the generic to "Needs<User>", and not "typeof needExample"

Playground

Upvotes: 2

Views: 249

Answers (1)

Jakub Švehla
Jakub Švehla

Reputation: 21

I am afraid that you're not able to do it. I already spend a few hours solving this inferring issue in the past. There is a problem in that you cannot infer just part of the object based on the different parts of the same object.

I found out that one of the potential working solutions is to change the API of the extend function to take two arguments and infer an argument2 from the argument1

Code:


class User {
    public name: string = ''
    public last = 3
    public x = true 
    public y = [1,2,3] 
}


export type Cast<T, U> = T extends U ? T : U

const extendUser = <Needs extends Record<string, { [K in keyof User]?: true }>>(
  needs: Needs,
  compute: {
    [K in keyof Needs]?: (a: { [ KK in  keyof Needs[K]]: User[Cast<KK, keyof User>] }) => void
  },
) => {
    return null as any  /* some runtime implementation */
}



extendUser(
    {
        lastName: { last: true, name: true },
        myName: { name: true }
    },
    {
        lastName: user => user.last
        //  ^?
        myName: user => user.name
        //  ^?
    }
)

Playground

I know it is not an answer to your question but I hope it will help you somehow. :)

I already had this problem when I was designign API for the library use-formio. Finally, I decided to change the API to those two arguments.

Upvotes: 1

Related Questions