Skip to content
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

Allow binding generic functions to a given type #37181

Closed
5 tasks done
mperktold opened this issue Mar 3, 2020 · 32 comments · Fixed by #47607
Closed
5 tasks done

Allow binding generic functions to a given type #37181

mperktold opened this issue Mar 3, 2020 · 32 comments · Fixed by #47607
Labels
Committed The team has roadmapped this issue Suggestion An idea for TypeScript

Comments

@mperktold
Copy link

Search Terms

generic function, parametric function, function type parameter, function type argument

The problem is closely related to #26043, but the proposed solution is different.

Suggestion

Currently, there is no way of explicitly providing type arguments to generic functions. For example, consider the following function:

function box<T>(value: T) {
  return { value };
}

TypeScript correctly infers the return type { value: T }. However, it's not possible to derive the return type for a particular T, say number, at least not without ugly workarounds such as creating new functions just to fix the type arguments:

type Box = ReturnType<typeof box>;  // { value: unknown }
type Box<T> = ReturnType<typeof ???>;  // can't propagate T from Box to box

const numBox = (n: number) => box(n);  // ugly workaround
type NumBox = ReturnType<typeof numBox>;

So I suggest to provide a syntax that allows to do just that: fix type arguments of generic functions:

type Box<T> = ReturnType<typeof box<T>>;
type NumBox = ReturnType<typeof box<number>>;

This syntax resembles the existing one for providing type arguments when calling a function:

const b = box<number>(2);

With this proposal implemented, interpreting such a call could be split into two parts:
First fix the type argument of box to number, which yields a function that takes a number.
Second, apply that function to 2.
Hence, it could also be written with parenthesis: (box<number>)(2).
I wouldn't suggest doing that in practice, but it shows that this proposal increases composability of generic functions.

Use Cases

I tried to come up with a way to implement discriminated unions more succintly. The idea is to start with factory functions and then combine the return types:

function val<V>(value: V) {
    return { type: "Val", value } as const;
}
function plus<V>(left: V, right: V) {
    return { type: "Plus", left, right } as const;
}
function times<V>(left: V, right: V) {
    return { type: "Times", left, right } as const;
}

type Expr<V> = ReturnType<typeof val<V>>
             | ReturnType<typeof plus<V>>
             | ReturnType<typeof times<V>>;

With this approach, you would get convenient factory functions for building instances, without duplicating the object structure.

But it doesn't work, because it relies on the new syntax I just proposed.

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Mar 4, 2020
@RyanCavanaugh
Copy link
Member

Grab a big coffee and read #6606 for some history on this.

One nit is that typeof genericFunc isn't a generic type (it's a regular type with generic call signatures), so it'd be wrong to provide it "type arguments". It's tempting to revisit (typeof f)(number) as a potential syntax, though. With the advent of conditional types, overloads are becoming less common and we might be able to get away with not supporting overload resolution for this process.

@mperktold
Copy link
Author

mperktold commented Mar 7, 2020

Grab a big coffee and read #6606 for some history on this.

Thanks for the pointer.
Quick summary on what's relevant for this issue:

The linked proposal was rejected because it was too complex, and many use cases it was originally thought for can now be solved using other features like indexed and conditional types.

It's still not possible to infer the type of all possible expressions.
Two interesting use cases that are still not possible is to access inferred return type of generic or overloaded functions.

This proposal would solve the former, but not the latter.

I don't know the code base, but I imagine this proposal would be relatively easy to implement, at least when compared to #6606.

One nit is that typeof genericFunc isn't a generic type (it's a regular type with generic call signatures), so it'd be wrong to provide it "type arguments".

When I wrote typeof func<T>, I meant that as typeof (func<T>), not (typeof func)<T>.

To elaborate, let's reconsider the box example.

box has type <T>(value: T) => { value: T }.
Then box<number> has type (value: number) => { value: number }, and typeof box<number> would return exactly that.

I guess what I'm trying to say is that func<T> narrows the type of a function on the expression level, and using typeof you can access the type of that expression.

As I said, this is really just taking what already happens in calls like box<number>(2), but decoupling the generics-binding syntax from the function call syntax.

It's tempting to revisit (typeof f)(number) as a potential syntax, though.

That's another interesting possibility I haven't thought about. It relies on type-level function calls being implemented, and uses typeof to lift the resulting expression to the type level.

The advantage of such a type-call syntax would be that you don't need ReturnType anymore.

On the other hand, you have to provide types for all arguments even if the return type doesn't depend on them. Contrived example:

async function boxAllWhenAlso<T>(vals: Promise<T[]>, promise: Promise<any>) {
    await promise;
    return (await vals).map(box);
}

To access the inferred return type Promise<Box<T[]>> for T=number with call syntax you need (typeof boxAllWhenAlso)(Promise<number[]>, Promise<any>), whereas with type arguments you need ReturnType<typeof boxAllWhenAlso<number>>.
So one needs extra Promise and [] symbols, while the other needs the ReturnType helper.

With the advent of conditional types, overloads are becoming less common and we might be able to get away with not supporting overload resolution for this process.

When overload resolution is out of play, I would like to come back to (typeof f)<number> for a moment: Would that really be so wrong?
I only thought about it on the expression level, but I think it would work on the type level as well.

I understand that typeof box is not a generic type in the same way as Promise would be, since the type variable of the function signature is still free.
But isn't that exactly the reason it makes sense to provide it with one?

<T>(value: T) => { value: T } is essentially a higher-kinded type, and by providing it with a T=number, we could get an ordinary type (value: number) => { value: number }.

@SimonMeskens
Copy link

type ReturnTypeMapped<T, U> = T extends (value: U) => infer V ? V : never;

type Test2 = ReturnTypeMapped<<T>(value: T) => { value: T }, string>; // type Test2 = `type Test2 = { value: unknown; }`

This is a ReturnType which allows you to pass in the value, but it's not mapped to the output correctly. I wonder if you are closer to making this sample potentially work @RyanCavanaugh? It seems like when I poke at this, it behaves way nicer than in the past, some great work was done surrounding this issue it seems.

Am I correct in understanding that if the above sample works, we probably wouldn't even need extra syntax as it gives us that extra step to grab a return type given an input?

@SimonMeskens
Copy link

Sorry for the double email, but I'd like to add another sample with expected behavior

type ReturnTypeMapped<T, U> = T extends (value: U) => infer V ? V : never;

type Lambda = <T>(value: T) => { value: T }; 

type Test1<T> = ReturnTypeMapped<Lambda, T>;
// Expected: `type Test1<T> = ReturnTypeMapped<<T>(value: T) => { value: T }, T>;`
// In other words, it doesn't try to collapse the type yet here.

type Test2 = ReturnTypeMapped<Lambda, string>;
// Expected: `type Test2 = { value: string; }`

type Test3 = Test1<string>
// Expected: `type Test2 = { value: string; }`

@bradennapier
Copy link

bradennapier commented Apr 8, 2020

so where your example works for return type @SimonMeskens i do not thing it works for other types of infer whereas I do believe the proposed solution would fix my request which is to use the returned type from box<string> to infer with #37835 .

Essentially, the goal imo would be to have:

function box<T>(value: T) {
  return { value };
}
typeof box = <T>(value: T) => { value: T }

then we can derive a new type by using (typeof box)<string>

typeof box<string> = <string>(value: string) => { value: string }

or in my more complex example:

type ForgotPasswordData = {
    readonly query: 'forgot';
};

type ConfirmEmailData = {
    readonly query: 'confirm';
    code: string;
}

const presets = {
    forgotPassword(data: ForgotPasswordData): any { },
    confirmEmail(data: ConfirmEmailData): any { }
} as const

type Presets = typeof presets

declare function sendEmailTemplate<P extends keyof Presets>(
  preset: P,
  data: Parameters<Presets[P]>[0],
): Promise<ReturnType<Presets[P]>>

type GetDataTypeForPreset<
  P extends Parameters<typeof sendEmailTemplate>[0],
  F  = (typeof sendEmailTemplate)<P>
> = F extends (preset: P, data: infer R) => any ? R : never;

means that we can tell Typescript what the generic will be for the function so it can tell us what another argument (or the return type) will be in turn

@SimonMeskens
Copy link

You seem to be asking for a completely different thing. I suggest opening a new issue.

@bradennapier
Copy link

bradennapier commented Apr 8, 2020

Not sure how it is any different? The only difference is the desire to be able to use it to infer other values which may have their values determined by the generic - but it seems it would be the same feature which allows for both unless im completely misunderstanding.

In my case my function essentially turns into:

declare function sendEmailTemplate<'forgotPassword'>(
  preset: 'forgotPassword',
  data: ForgotPasswordData,
): Promise<any> 

declare function sendEmailTemplate<'confirmEmail'>(
  preset: 'confirmEmail',
  data: ConfirmEmailData,
): Promise<any> 

So using typeof sendEmailTemplate<'confirmEmail'> would then allow us to use infer to get the type of data as ConfirmEmailData

type GetDataTypeForPreset<
  P extends Parameters<typeof sendEmailTemplate>[0],
  F  = typeof sendEmailTemplate<P>
> = F extends (preset: any, data: infer R) => any ? R : never;

type ConfirmDataType = GetDataTypeForPreset<'confirmEmail'>
// ===
type GetDataTypeForPreset<
  P = 'confirmEmail'
  F  = <'confirmEmail'>(
      preset: 'confirmEmail',
      data: ConfirmEmailData,
    ): Promise<any> 
> = F extends (preset: any, data: infer R) => any ? R : never;

@SimonMeskens
Copy link

I'm not entirely sure what you are proposing, since you seem to keep changing your posts.

@bradennapier
Copy link

Just added a drill down didn't change any of the actual content

@Harpush
Copy link

Harpush commented Jun 5, 2020

Encountered this issue while trying to solve a problem concerning ReturnType with generics.
The problem is actually the difference between:

type A<T> = (value: Promise<T>) => {value: T};
type B = <T>(value: Promise<T>) => {value: T};

Currently typeof creates the the second line while what we actually want is to somehow get the first one.
Considering this, a good syntax as said above could be type C<T> = (typeof someFunc)(T); Which will create the second line. That way ReturnType will just work.

Most use cases i got into are:

export function createSomething(paramOne: Promise<string>, paramsTwo: Promise<boolean>) {
    // Return some big complex object i dont want to create an interface for...
}

type Something = ReturnType<typeof createSomething>; // Works great!

export function createSomething2<T extends string>(paramOne: Promise<T>, paramsTwo: Promise<boolean>) {
    // Return some big complex object i dont want to create an interface for using T...
}

type Something2 = ReturnType<typeof createSomething2>; // Doesnt work :(
// What i actually need
type Something2<T extends string> = ReturnType<(typeof createSomething2)(T)>;

@Jack-Works
Copy link
Contributor

function x<T>(a: T): {x: T} {}

type y = typeof x
// Today: type y = <T>(a: T) => void
// Proposed: type y<T> = (a: T) => void

type z<T> = typeof x
// Today: type z<T> = <T>(a: T) => void
// Proposed: no change

The generics "moved" from the right side to the left side (if possible, aka no overload).

type w = ReturnType<y<number>>
  • Today: Type 'y' is not generic.(2315)
  • Proposed: w is {x: number}

@mperktold
Copy link
Author

@Jack-Works I think your approach isn't quite working. Your type y wouldn't be a propper type, since it depends on a type T which you neither defined as a type parameter nor provided as a type argument.

Also, while z<T> does evaluate to type z<T> = <T>(a: T) => void, that doesn't seem very helpful, since the inner T shadows the outer one you are declaring. So basically y and z<T> behave exactly the same. The only difference is that z has an unused type parameter.

With my proposal implemented, you could achieve the wanted behavior as follows:

type y = typeof x
// Today: type y = <T>(a: T) => void
// Proposed: no change

type z<T> = typeof x<T>
// Today: syntax error
// Proposed: type y<T> = (a: T) => void

Note that the function type of z<T> doesn't have its own <T> type argument anymore, so the argument really must match the type you provide to z.

@mperktold
Copy link
Author

Not sure how it is any different? The only difference is the desire to be able to use it to infer other values which may have their values determined by the generic - but it seems it would be the same feature which allows for both unless im completely misunderstanding.

@bradennapier You're right, you show how this feature would be useful for infering parameter types as well, not only return types.

This is another key difference between this proposal and the (typeof func)(T) syntax proposal, since the latter would always yield the return type, and never a parameter type.

@Harpush
Copy link

Harpush commented Sep 8, 2020

Any news concerning this proposal?

@keyvan-m-sadeghi
Copy link

keyvan-m-sadeghi commented Sep 11, 2020

@RyanCavanaugh I read most of #6606, (typeof f)(number) sounds a legit solution, or more generally adding a AFunction<string>(number) type syntax. This looks to fall perfectly in line with the rest of the syntax, like AnObject['member'], and seems to be very useful for functional programming (as well as overloading). Why has it been dismissed?

@warent
Copy link

warent commented Sep 27, 2020

I just created a (duplicate) ticket where I proposed the alternate syntax of typeof<T> fn so there's some food for thought.

Also noted that this could enable us to define/apply the generic as well. e.g.

const f = <T extends any>(id: T): T => id;
type F = Parameters<typeof<"hello"> f>[0];

This would make F strictly typed to "hello".

An interesting additional benefit of this is that we could partially apply the generics for a kind of "generic type-currying"

@Harpush
Copy link

Harpush commented Sep 27, 2020

I am really waiting for this... A big limitation in my project

@pie6k
Copy link

pie6k commented Nov 3, 2020

I was trying to find a way to connect generic type with parent type, but with no luck

type Callable<R, G> = <A extends G>(...args: any[]) => R;

type GenericReturnType<G, F> = F extends Callable<infer A, infer G> ? A : never;

It's still resolved to unknown

Leaving here for other folks

@migueloller
Copy link

Just wanted to chime in and add a use case that my team ran into.

When we compose React components together we infer component props so that we don't have to import them and/or change them whenever component APIs change. For example:

type Props = ComponentProps<typeof 'button'>

function Button(props: Props) {
  return <button {...props} />
}

In this case, typeof 'button' could've actually been typeof SomeOtherReactComponent. Now, here's the issue:

// Component1.tsx

type Component1Props<T> = {
  value: T
}

function Component1<T>(props: Component1Props<T>) {
  // implementation
}

// Component2.tsx

import Component1 from './path/to/Component1'

type Component2Props<T> = ComponentProps<typeof Component1> // uhhh, where do we put `T`?

function Component2<T>(props: Component2Props<T>) {
  return <Component1 {...props} />
}

This example might seem contrived because I've tried to keep it generic and simple, but in practice this happens with form components that are generic on their props. (i.e., there's options and onSelect props that use a generic T such that onSelect looks like (option: T) => void and options looks like T[]).

So ideally we'd be able to do this:

// Component2.tsx

type Component1Props<T> = (typeof Component1)(T) extends (props: infer P) => JSX.Element ? P : never

type Component2Props<T> = Component1Props<T>

@imjordanxd
Copy link

any update on this?

@Harpush
Copy link

Harpush commented Jun 15, 2021

@RyanCavanaugh Is there any chance to revisit the syntax you proposed?

@mperktold
Copy link
Author

I'm not sure whether my proposal was fully clear, and since it seems we're talking syntax again, I thought I'd give some more explanations:

The syntax I'm proposing is just f<x>, where f is a function with a type parameter and x is a type argument.

Such an expression would yield f as a value, but with a different type, namely with the given type argument substitutet for its parameter.

You could think of it as casting f to a more narrow type without a type parameter, but in a typesafe way:

const identity = <T>(arg: T) => arg;      // has type <T>(arg: T) => T
const stringIdentity = identity<string>;  // has type (arg: string) => string
const stringIdentity2 = identity as (arg: string) => string;  // equivalent but ugly

Thus, the syntax does not involve typeof, although typeof can be used to get the type of the expression as usual.

Actually, it's not really "new" syntax, but the existing syntax for calling generic functions, like identity<string>("x"), and therefore easy to learn for developers.

Maybe it's just me, but to me this really seems like the most natural way to enable this feature in TypeScript. In fact, before creating this proposal, I tried the proposed syntax and I wouldn't have been surprised if it would have worked.

@DavideValdo
Copy link

DavideValdo commented Jun 16, 2021

So, in the end, are you suggesting to add a utility type?

Why not just use anonymous functions for this?

type Identity<T> = (a: T) => T

const myFunction: Identity<string> = (a) => "" // (a: string) => string

@mperktold
Copy link
Author

So, in the end, are you suggesting to add a utility type?

@DavideValdo Not really. I'm suggesting to add a way to concretize the type of a generic function.

The example with identity should demonstrate how the suggested feature would work, but it's not a use case where I would really need it. Even this works fine as well:

const identity = <T>(arg: T) => arg;
const stringIdentity: (arg: string) => string = identity;

Better use cases are the one provided in the linked issue by @bradennapier, or the one in my proposal about discriminated unions. In general, it would be useful to help TypeScript infer the correct type without providing it explicitly.

I agree that this is probably not needed very often, but when it is, it would very useful.
Also, I don't think it would be a big change, at least not on the surface of the language, so it could be well worth the effort.

@sandorvasas
Copy link

a.k.a template specialization as known in C++

@tavindev
Copy link

Any progress?

@deviprsd
Copy link

+1

2 similar comments
@SlavikSurminskiy

This comment has been minimized.

@SlavikSurminskiy

This comment has been minimized.

@alamothe
Copy link

Some interesting workarounds are listed here: https://stackoverflow.com/questions/50321419/typescript-returntype-of-generic-function

But they're not pretty

@ahejlsberg
Copy link
Member

This feature is now implemented in #47607.

@mwiemer-microsoft
Copy link

Can we update the tags here to reflect that it was accepted?

@RyanCavanaugh RyanCavanaugh added Committed The team has roadmapped this issue and removed In Discussion Not yet reached consensus labels May 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Committed The team has roadmapped this issue Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.