Repository navigation
[Bug] & [Feature Request] ?? Compose Function Types using Generics / inferring fn params using generics #37835
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 (@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 genericTis assignable to fn's genericT -
typeof fn<T>would essentially create a type signature where the genericTis substituted infnand 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
inferbased 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 usingtypeofon 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)
Activity
[-][Bug] & [Feature Request] ?? Infer Parameters using Generics[/-][+][Bug] & [Feature Request] ?? Compose Function Types using Generics / inferring fn params using generics[/+]DanielHoffmann commentedon May 19, 2020
Have same problem, I came up with a simpler example:
I want to make a new function that is exactly the same as fn, but where the first parameter is string instead of number. This example doesn't work because fn is generic and
<typeof fn<T>>doesn't workUnrelated to this, is there a way to
range()over a tuple type? For example in this example instead ofb: Parameters<typeof fn<T>>[1]do...args: Parameters<typeof fn<T>>[1..]to get all parameters after the first one? Would be helpful if more arguments get added tofnin the futuremperktold commentedon Jun 4, 2020
Daniel Hoffmann Bernardes (@DanielHoffmann) I could make this work using the following code (Playground Link):
You can achieve this using constructs like
[T, ...A]whereA extends any[].I did something similar in
withStringArg, so it can wrap any function whose first parameter is a number. All other parameters and their type are left as they are.mperktold commentedon Jun 16, 2021
Braden Napier (@bradennapier)
I don't think this is a problem if it binds like
typeof (fn<T>). Thenfn<T>would need to be its own thing, which is exactly what I am proposing, since it could be useful on its own.Agree! 😃
Jack-Works commentedon Jun 5, 2022
Looks like this issue is fixed by 4.7 new feature instantiation expression?
bradennapier commentedon Jun 15, 2022
Well it actually is not - but it is close enough! It models half of what i needed.