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

New type param modifier to allow for partial inference based on signature declaration #53999

Open
5 tasks done
Andarist opened this issue Apr 24, 2023 · 6 comments
Open
5 tasks done
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@Andarist
Copy link
Contributor

Suggestion

🔍 Search Terms

partial inference modifiers type parameters

✅ Viability 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, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Type inference is a powerful tool but for certain scenarios it suffers from a usability issue - it's all or nothing and doesn't allow for "partial application".

There are two angles here:

  1. users sometimes would like to partially apply the type arguments list
  2. library authors would like to accept partial type arguments list

When it comes to the first one there is already an open proposal for it here and even a PR implementing it here. Based on reactions below both of those I think it's fair to say that it's a highly requested feature.

However, even if that feature would get its way to the language it would still suffer from some usability issues for the mentioned second use case. The problem is that this feature would require an explicit opt-in by the user to leverage this feature. I think that's an acceptable tradeoff for library authors - we certainly wouldn't be super picky about it if that feature would land.

I think though that this could easily be improved by allowing a special type parameter modifier - like preferinfer or just infer:

declare function get<T, preferinfer K extends keyof T>(obj: T, key: K): T[K]

// `T` provided as `User`, `K` inferred as `name`
const res = get<User>({ name: 'Andarist', age: 32 }, 'name') 
res // string

Both use cases are very much related but I feel like they are orthogonal as one can exist without the other. This proposal focuses only on the second use case. A draft implementation for the feature can be found here.

📃 Motivating Example

In XState we accept a couple of type arguments but we'd also like to additionally perform some additional inference/validation based on the received config argument. It's a JSON-like schema:

declare function createMachine<
  TEvent extends { type: string },
  preferinfer const TConfig extends MachineConfig<TEvent>
>(config: TConfig): StateMachine<TConfig, TEvent>

type MyEvents = { type: 'INC' }

createMachine<MyEvents>({
  initial: 'foo',
  states: {
    foo: {
      on: {
        INC: 'bar', // should be allowed
        OTHER: 'bar', // should be an error
      }
    },
    bar: {}
  }
})

This would be especially useful for us in combination with the recently introduced const type parameters.

At the moment we have to resort to this, rather silly, workaround:

createMachine({
  types: {
    events: {} as MyEvents // workaround here
  },
  initial: 'foo',
  states: {
    foo: {
      on: {
        INC: 'bar', 
        OTHER: 'bar',
      }
    },
    bar: {}
  }
})

Drawbacks of this approach:

  • this puts some unnecessary pressure on TS to infer things that we don't really want to infer. They are meant to be explicitly given by the developers.
  • the types also become part of the inferred TConfig
  • it's easy to accidentally infer TEvent from locations that shouldn't be used as inference sources. A rogue any somewhere added to inference candidates might implicitly deopt all locations. We have to use NoInfer trick to avoid this

Another workaround for this issue is to change the API, use factory functions to "bind" explicit type parameters to the returned function. This alters the API though and I don't think that's really a suitable workaround when better alternatives could exist.

IIRC, Redux Toolkit also uses a similar pattern in its types to "accept an explicit type" for TState. And I distinctly remember that Tanner from Tanstack would give a lot for a feature like this (perhaps this would be more useful for Tanstack Table than Tanstack Query). I'm pretty sure that a ton of OSS library authors would just love, love to see a solution for this to land in some shape.

@phryneas
Copy link

phryneas commented Apr 24, 2023

This would be a dream come true, for Redux Toolkit, but also so many other situations where I have to make the decision between "do I write this for inference" or "do I write this for the user to pass something in".
Recently we added 5 overloads to a function in Apollo Client - one for inferred parameters and 4 for explicitly written parameters, to mimic the behavior somewhat. (Link to PR)
With this, we wouldn't need any overload, just a single signature.

@ssalbdivad
Copy link

Looks amazing 😍

@cevr
Copy link

cevr commented Apr 24, 2023

I can't imagine a library that uses generics heavily that wouldn't benefit from this.

@svr93
Copy link

svr93 commented Apr 24, 2023

PR with investigation: link

@phryneas
Copy link

@svr93 that's the "opposite" PR - the PR that you linked requires the user of a type to write "infer this argument", while this is discussing a way that would enable the author of a type to declare "this argument should be inferred when the type is used".

This has been investigated in #52241.

@svr93
Copy link

svr93 commented Apr 26, 2023

@phryneas
The mechanics of inferring will be work the same way. Also, see this comment

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

6 participants