I'm encountering a problem with Typescript regarding the inference of types in a generic function.
Here's a simplified version of the code:
type FnObj<I extends z.ZodType> = {
input: I,
func: (input: I extends z.ZodType<infer U> ? U : never) => void
}
type Objs<I extends z.ZodType = z.ZodType> = {
[key: string]: FnObj<I>
}
function createFormDef<FF extends Objs>(formObject: FF) {
return formObject;
}
createFormDef({
test: {
input: z.object({
d: z.number(),
}),
func: (input) => {
input.dsf // This line is not throwing an error, although it should be typed as {d: number}.
},
},
});
I've tried remove generic of Objs to make the single key its own type. But it didn't work. as follows: (also removed the zod part)
type FnObj<I extends any = any> = {
input: I,
func: (input: I) => void
}
type Objs = {
[key: string]: FnObj
}
function createFormDef(formObject: Objs) {
return formObject;
}
createFormDef({
test: {
input: {
hello: 10
},
func: (input) => {
input.dsf // still not working, should typed as {hello:number}
},
},
});
TypeScript doesn't support existentially quantified generics, so you can't say "an object whose property values are FnObj<I>
for some I
I don't care about". Writing FnObj<any>
(which is what your FnObj
becomes with your default type argument) doesn't accomplish that, because the any
type is not safe, and so FnObj<any>
allows things where input
and func
are completely unrelated.
Instead of trying to write such a type, it would be better to say "an object whose property values at key K
are FnObj<T[K]>
for a T
that I specify". That is, you would map over an object type T
to get a corresponding Objs<T>
:
type Objs<T extends object> = { [K in keyof T]: FnObj<T[K]> }
function createFormDef<T extends object>(formObject: Objs<T>) {
return formObject;
}
Now the compiler can infer T
from the formObject
input:
const x = createFormDef({
test: {
input: {
hello: 10
},
func: (input) => {
input.dsf // error
},
}
});
x.test.input
// ^?(property) input: { hello: number; }
Here T
is inferred as {test: {hello: number}}
, and thus x
is Objs<T>
which is {test: FnObj<{hello: number}>}
, and thus the compiler can contextually type the input
parameter to the fn
callback as {hello: number}
.