Description
Note: This got a bit verbose and detailed -- the Use Case
at the bottom may be the clearest example of the goal here.
Hey y'all!! Running into a situation where I need to infer a property of a function but it needs to use another property (using a Generic) of the function to properly determine the type. I am guessing this isn't currently possible based on my testing but think it would be a pretty good thing to allow?
declare function sendEmailTemplate<P extends keyof Presets>(
recipient: string | Parameters<typeof sendSESEmail>[0],
preset: P,
data: Parameters<Presets[P]>[0],
): Promise<ReturnType<Presets[P]>>
type GetDataTypeForPreset<
P extends Parameters<typeof sendEmailTemplate>[1],
> = typeof sendEmailTemplate extends (recipient: any, preset: P, data: infer R) => any ? R : never;
TypeScript Version: 3.8.3
Search Terms:
- infer parameters based on property
- infer function parameters using generics
- generic infer parameters
Code
Typescript Playground
Expected behavior:
Ability to compose types to extract an argument from a function which is able to utilize the value of a generic to do so.
Actual behavior:
There are some weird bugs where the types end up being wrong in multiple situations - sometimes it returns never when it shouldnt etc. This appears to have to do with the types that data
are. If all the types of the possibilities are not compatible then it will not return anything which does not seem to be correct (@see typescript playground where it returns both as never
now)
Essentially what would be nice, is given a function:
declare function sendEmailTemplate<P extends keyof Presets>(
recipient: string | Parameters<typeof sendSESEmail>[0],
preset: P,
data: Parameters<Presets[P]>[0],
): Promise<ReturnType<Presets[P]>>
We have the ability to get the function with static types by defining the generics like we can do when calling the functions. So essentially it may look like this? not sure...
type GetDataTypeForPreset<
P extends Parameters<typeof sendEmailTemplate>[1]
> = (typeof sendEmailTemplate)<P> extends (recipient: any, preset: any, data: infer R) => any ? R : never;
Similar Issues:
#37181 is similar
Proposal
So inline with #37181 - but expanding on it a bit, the idea would be to allow composing a type by providing the values of the generics for a function and receiving a typeof fn
which would be the signature it would have should that generic be provided to it.
To visualize this I simplified the case here a bit. Clearly there are ways to achieve this specific functionality but the concept of being able to pass generics which can be passed down has wide implications and shouldn't be too complex to implement (although I don't really know that is the case lol)
So given the below (which currently doesnt work as FooProps is never
)
type PropTypes = {
foo: { action: 'foo', payload: { value: number } },
bar: { action: 'bar', payload: { value: string } }
}
declare function fn<T extends keyof PropTypes>(prop: T, props: PropTypes[T]): any
type GetPropsFor<
T extends Parameters<typeof fn>[0]
> = typeof fn extends (prop: T, props: infer V) => any ? V : never;
type FooProps = GetPropsFor<'foo'>
the goal is that the generic can be used to infer the fn. When calling the expression this does work as expected:
fn('foo', { action: 'bar', payload: { value: 'oops' } }) ; // error as it expects foo's props not bar's.
So essentially with this proposal this would evaluate like this:
type PropTypes = {
foo: { action: 'foo', payload: { value: number } },
bar: { action: 'bar', payload: { value: string } }
}
declare function fn<T extends keyof PropTypes>(prop: T, props: PropTypes[T]): any
type GetPropsFor<
T extends Parameters<typeof fn>[0]
> = typeof fn<T> extends (prop: T, props: infer V) => any ? V : never;
type FooProps = GetPropsFor<'foo'>
A couple things this would enforce:
-
type GetPropsFor<T> = typeof fn<T> ...
would require that GetPropsFor's genericT
is assignable to fn's genericT
-
typeof fn<T>
would essentially create a type signature where the genericT
is substituted infn
and the resulting value and return type is provided in response.
So given
declare function fn<T extends string | number>(arg: T): T[]
thentypeof fn<string>
would becomedeclare function fn<T extends string>(arg: T): T[]
-
It would be possible in these cases to define less generics than the fn itself defines in the case we simply want to define a specific value. I believe flow has a concept for this using
_
wherefn<_, string, _>
indicates we dont want to define the values for the first or third generics and they should be their default flow docs -
In addition, any values which utilize the generics will be calculated so that we can
infer
based upon that. In this way
type GetPropsFor<
T extends Parameters<typeof fn>[0]
> = typeof fn<T> extends (prop: T, props: infer V) => any ? V : never;
type FooProps = GetPropsFor<'foo'>
will become { action: 'foo', payload: { value: number } }
Use Case
So there are a ton I can come up with I can imagine with this as I have run into it a few times now running through code - but one of the main things is when I want to strictly type parameters or values but then provide helpers/utilities which may call those functions on behalf of the caller.
type PropTypes = {
foo: { action: 'foo', payload: { value: number } },
bar: { action: 'bar', payload: { value: string } }
}
declare function sendEmailType<
T extends keyof PropTypes
>(toEmail: string, emailType: T, props: PropTypes[T]): any
class User {
// user database values
public async function sendEmail(
emailType: Parameters<typeof sendEmailType>[0],
props: Parameters<typeof sendEmailType>[1]
) {
return sendEmail(this.email, emailType, props)
}
}
The problem is that we have now lost a significantly amount of type safety which we had before which essentially binds the value of emailType
to props
. There are many other cases where that tight coupling being maintained would be very beneficial, if more are needed I am sure I can come up with the other areas I have run into this!
We would instead want user.sendEmail('foo', { action: 'foo', payload: { value: 1 } })
to fail since it should know that we will be calling sendEmailType
with foo
as the emailType
So even without infer in this case we could simply write the above as
class User {
public async function sendEmail<
T extends Parameters<typeof sendEmailType>[0]
>(
emailType: T,
props: Parameters<typeof sendEmailType<T>>[1]
) {
return sendEmail(this.email, emailType, props)
}
}
Other Potential Syntax
Since there are potential issues with typeof fn<T>
since typeof fn
is a type without generics, there are a few ways around that but i think the given syntax is the easiest if there is a way to bind them.
- Introduce
as
(or another word) keyword as option when usingtypeof
on fns(typeof fn as fn<A, B, C>)
type FnOfType<T> = typeof fn as fn<T>
// or some variation...
type FnOfType<T> = typeof fn with fn<T>
type FnOfType<T> = typeof fn with <T>
type FnOfType<T> = typeof fn is fn<T>
type FnOfType<T> = typeof fn*<T>
- ... (will update if more come to mind)