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 currying of generics #30123

Closed
essenmitsosse opened this issue Feb 27, 2019 · 8 comments
Closed

Allow currying of generics #30123

essenmitsosse opened this issue Feb 27, 2019 · 8 comments
Labels
Needs More Info The issue still hasn't been fully clarified

Comments

@essenmitsosse
Copy link

First thing: I know there are a lot of question and good answers about how to write, curried functions. What I am requesting here is to be able to curry generics.

This seems related to #10571, but I thing its a slightly different proposal.

For this example we use this interface and this simple Instance

interface Interface {numberKey: number, stringKey: string}
const instance: Interface = {numberKey: 1, stringKey: "a"}

What is expected

It would be greate to be abled to write a function like this

// 1. Take a key name and a value
// 2. Return a function, that takes an object, 
//    that has a key that matches the given value
const getTransformerA = <I><K extends keyof I>(key: K, value: I[K]) =>
        (input: I) => input

// We now we should be able to use it like this
const transformerA1 = getTransformerA<Interface>( 'numberKey', 123 )

// And we expect an error here
const transformerA1 = getTransformerA<Interface>( 'stringKey', 123 )

The advantage of this, is that we get a return function, that is guaranteed to work with a certain interface, before we call the return function. Which is especially useful in cases, where we could loose typing, but still want to ensure compatibility with an interface.

But this syntax doesn't work and I seem unable to find a workaround.

@essenmitsosse
Copy link
Author

Here is what I already tried:

Version A

const getTransformerA =
    <I extends {[key in K]: I[K]}, K extends keyof I> (key: K, value: I[K]) =>
        (input: I) => input

// We now can use it like this
const transformerA1 = getTransformerA<Interface, 'numberKey'>( 'numberKey', 123 )

// This correctly gives a type error
const transformerA1 = getTransformerA<Interface, 'stringKey'>( 'stringKey', 123 )

// Problem: 
//We verbosely have to pass the name of the string as a type argument

Version B

const getTransformerB = < K extends string, Value > ( key: K, value: Value ) =>
        (input: {[key in K]: Value}) => input

// Now this works which is much easier
const transformerB1 = getTransformerB( 'numberKey', 123 )

// But this gives a false positive, since we haven't defined which interface we are relating to
// It is to generic.
const transformerB1 = getTransformerB( 'stringKey', 123 )

// Problem: 
// False positives, so no guarantee to be compatible with a certain interface.

Version C

const getTransformerC = <I extends {[key: string]: any}>() =>
    <K extends keyof I>(key: K, value: I[K]) =>
        (input: I) => input

// Works all as expected
const transformerC1 = getTransformerC<Interface>()('otherKey', 123) 
const transformerC2 = getTransformerC<Interface>()('someKey', 123) 

// Problem:
// We changed the runtime. Now we have an unnecessary function call.

I also tried to use default values for type parameters, but couldn't get anything to work, that was improving on the above versions.

@jack-williams
Copy link
Collaborator

jack-williams commented Feb 27, 2019

I'm not sure I follow your example. What is the proposed type of transformerA1? You have partially applied the outer generic, so is transformerA1 a generic function?

I think this might be related, although not a duplicate: #29043

@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Feb 27, 2019
@RyanCavanaugh
Copy link
Member

What do you intend to do with transformerA1 ?

@essenmitsosse
Copy link
Author

essenmitsosse commented Feb 27, 2019

@jack-williams yes I agree, looks related even though its not exactly the same.

Here is an example with a function, that actually does something so maybe it becomes more clear:

interface Interface {numberKey: number, stringKey: string}
const instance: Interface = {numberKey: 1, stringKey: "a"}

const getTransformer = 
  <I><K extends keyof I>(key: K, newValue: I[K]) => // - Take a key name and a value.
    (input: I) =>                                   // - Take an object.
      Object.assign({}, input, {[key]: newValue});  // - Return a new object, with the
                                                    //   new value assigned to the key;

const transformerA1 = getTransformer<Interface>( 'numberKey', 123 );
// transformerA1 now takes any Object that implements Interface, 
// and changes the key "numberKey" to the value 123;

const newInstance = transformerA1( instance );
instance.numberKey    // 1
newInstance.numberKey // 123

// If we try to build a transformer like this, we should get a type error
const transformerA2 = getTransformer<Interface>( 'stringKey', 123 );
// Expected Error: 
// Argument of type '123' is not assignable to parameter of type 'string'.ts(2345)

Note this is written with the syntax that is currently not working getTransformer <I><K extends key of I>() => ...

Hope everything else was covered in the opening post.

@jack-williams
Copy link
Collaborator

jack-williams commented Feb 27, 2019

Thanks for the expanded example. I think I understand your aim, though I'm not really clear on what difference this gives you vs #26349 (partial type inference with sigil).

const getTransformer = <I,K extends keyof I>(key: K, newValue: I[K]) =>  (input: I) =>  ... 
const transformerA1 = getTransformer<Interface,_>( 'numberKey', 123 );
const transformerA1 = getTransformer<Interface>( 'numberKey', 123 ); // or this, if the syntax filled in _

It seems like this does the same thing, more-or-less. Given that you apply the function immediately you never really materialise the partially applied generic, so it seems breaking it apart doesn't help much. I might be wrong on this though.

If you had higher-kinded types then I think this proposal would be notably different to partial type inference.

@essenmitsosse
Copy link
Author

essenmitsosse commented Feb 28, 2019

const transformerA1 = getTransformer<Interface>( 'numberKey', 123 ); // or this, if the syntax filled in _

This would indeed almost be the same. The main difference to what I was looking for was the function making it a bit more clear which parameters should be set (the interface) for the function to properly work, while the other two parameters should never have to be set, because there should be almost not case where it would be necessary to set them.

Like I said, the main advantage of the whole construct is, to have kind of type-saftey in cases, where the typying will be lost - meaning we want to ensure the function is compatible with an interface, before we call it (otherwise we could just go for Version B).

Leaving the underscores there would definitely work and not kill anyone, but neither would repeating the string (Version A).

But non of these version seem really descriptive to me, about how the function is supposed to be used. The definition of K ist just in the type parameters because thats where we define generics. Not because we want it to be a parameter.

So maybe thats also the real issue here - Type parameters seem to serve two usecases (1. allowing configuration, 2. allowing interference), making it hard to see as a user of a function how it is meant to be used.

@c-vetter
Copy link

Before finding this, I have tried to tack this very idea onto #26242 because I think both try to solve overlapping issues and making them them work hand in hand would be worthwhile.

@essenmitsosse and others interested in this feature may want to chime in soon since #26242 has recently been added to the project team's agenda.

@RyanCavanaugh
Copy link
Member

Agree this is basically a duplicate of #26242

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

4 participants