Skip to content

[Bug] & [Feature Request] ?? Compose Function Types using Generics / inferring fn params using generics #37835

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
bradennapier opened this issue Apr 8, 2020 · 5 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@bradennapier
Copy link

bradennapier commented Apr 8, 2020

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 generic T is assignable to fn's generic T

  • typeof fn<T> would essentially create a type signature where the generic T is substituted in fn and the resulting value and return type is provided in response.

So given declare function fn<T extends string | number>(arg: T): T[] then typeof fn<string> would become declare 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 _ where fn<_, 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.

  1. Introduce as (or another word) keyword as option when using typeof 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>
  1. ... (will update if more come to mind)
@bradennapier bradennapier changed the title [Bug] & [Feature Request] ?? Infer Parameters using Generics [Bug] & [Feature Request] ?? Compose Function Types using Generics / inferring fn params using generics Apr 8, 2020
@sandersn sandersn added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Apr 8, 2020
@DanielHoffmann
Copy link

DanielHoffmann commented May 19, 2020

Have same problem, I came up with a simpler example:

function fn<T>(a: number, b: T): T {
  return b;
}

function fnWrapper<T>(a: string, b: Parameters<typeof fn<T>>[1]): ReturnType<typeof fn<T>> {
  return fn(parseInt(a), b);
}

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 work

Unrelated to this, is there a way to range() over a tuple type? For example in this example instead of b: 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 to fn in the future

@mperktold
Copy link

mperktold commented Jun 4, 2020

@DanielHoffmann I could make this work using the following code (Playground Link):

function fn<T>(a: number, b: T): T {
  return b;
}

function withStringArg<A extends any[], R>(fn: (a: number, ...args: A) => R) {
    return (a: string, ...rest: A) => fn(parseInt(a), ...rest);
}

const fnWrapper = withStringArg(fn);

const b = fnWrapper("0", true);     // b has type `true`

Unrelated to this, is there a way to range() over a tuple type? For example in this example instead of b: Parameters<typeof fn>[1] do ...args: Parameters<typeof fn>[1..] to get all parameters after the first one? Would be helpful if more arguments get added to fn in the future

You can achieve this using constructs like [T, ...A] where A 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
Copy link

@bradennapier

Since there are potential issues with typeof fn since typeof fn is a type without generics

I don't think this is a problem if it binds like typeof (fn<T>). Then fn<T> would need to be its own thing, which is exactly what I am proposing, since it could be useful on its own.

but i think the given syntax is the easiest if there is a way to bind them.

Agree! 😃

@Jack-Works
Copy link
Contributor

Looks like this issue is fixed by 4.7 new feature instantiation expression?

@bradennapier
Copy link
Author

Well it actually is not - but it is close enough! It models half of what i needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants