Reputation: 271
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"
Upvotes: 2
Views: 249
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
// ^?
}
)
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