-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Named/keyed type parameters #54254
Comments
I thought the same thing, and a quick search indeed turned up at least one: #38913 |
Not only would this functionality be incredibly useful, the suggested syntax looks great! It implies the ordering is arbitrary, is clearly distinguishable from an object literal, and is about as concise as you could imagine. If one of the |
This feels ambiguous at the call site whether itβs calling |
@fatcerberus Good point, I didn't consider the ambiguity on invocation (I very rarely actually pass generic parameters to a function manually). Perhaps dropping the object literal syntax on invocation could work? fn<T: 100, T2: 'foo' >(100, 'foo') |
Ah, yes - this is a good point. I meant to mention this and I forgot. I think that it's fairly OK here since it feels quite natural and one could draw a parallel to pure JS: function test1(obj) { obj.a; obj.b }
function test2({ a, b }) { a; b } I understand that the latter kinda imitates named parameters through the object destructuring... but it still feels pretty close to me. I don't feel like this distinction is important to the users. JS supports a lot of things and new things might come to the language, like even some kind of extractors (@rbuckton's proposal: https://github.com/tc39/proposal-extractors ). With such things... we can never truly be sure how a thing gets interpreted based on the call site alone. However, I really don't mind any syntax. I'm pretty sure that we could figure out something that would satisfy the TS team. The main goal of this issue is to get some kind of interest in this feature and to start the conversation. As always... I would gladly work on the actual implementation for this :) |
I see your 5 generic type parameters and raise to 10, 8 of which are in a hacky version of such a bag that is an absolute pain to maintain and use in This feature would be phenomental. |
If #20126 or something like it were to be merged, then you could get this without any new syntax more or less. You could constrain a single type parameter to an object type whose keys are your desired βnamesβ and whose value types represent their constraints, and then use indexed access types to refer to them. Maybe not as ergonomic but it would work fine. But without inference itβs not feasible. |
@Andarist To be fair, the destructured-parameter is doing the same thing at the call site, namely passing an object value. Here TS would have to interpret the call site differently (single atomic object type vs. separate named types) depending on the signature and that might be a dealbreaker. Basically you have to know the type of This does give me an idea though: Maybe we could take a leaf from the JS parameter destructuring book and just make this a new sort of multivariate constraint (i.e. you constrain the properties individually); at the call site it would actually be a single object type and the implementation could just refer to the generics as EDIT: Heh, @jcalz beat me to it with almost the exact same idea. |
I kinda beat both of you here :P I already mentioned #20126 in the "use cases" section. This is a nice approximation of the requested feature but this feature aims to solve some of the problems of that solution. I listed type parameter defaults, partial inference, potential variance problems, and type parameter modifiers as things that are not covered by it. |
Oh so now I'm supposed to read things before I reply to them? |
Love this, just want to clarify some of the usages we'd want this to cover. 1) Accessing Other KeysSince the following is possible with ordered generics: type MyType = { myField: number; otherField: string }
// works:
declare function fnA<A extends MyType, B extends Omit<A, 'myField'>>(a: A, b: B): void
// also works:
declare function fnB<A extends Omit<B, 'myField'>, B extends MyType>(a: A, b: B): void I think I'd expect there to be no issue accessing other keys: declare function fnC<{ A extends MyType; B extends Omit<A, 'myField'> }>(a: A, b: B): void However, to do this with normal structured types, you need access to the type itself: interface MyInterface {
a: MyType;
// works:
b: Omit<MyInterface['a'], 'myField'>;
// doesn't work because `a` itself isn't declared:
// b: Omit<a, 'myField'>;
} I def don't know, but I feel like this could complicate things or make this require a different syntax. 2) Partial InferenceJust to clarify partial inference as @Andarist mentioned, with this inference working: declare function fnA<{ A extends string, B extends A[] = [A] }>(a: A): B
fnA('a') // ['a'] Then, if #54047 lands, I'd expect this to just work: declare function fnA<{ A extends number; B extends A[] = [A] }>(a: A): B
fnA<{ A: 'a' }>('a') // ['a'] 3) Passing Generic BagsWith large generic bags, it's convenient to be able to pass the whole bag around. The proposed syntax doesn't look like it would really cover this. declare function fnA<{ A extends MyType; B extends Omit<A, 'myField'> }>(
a: A,
b: B
): MyReturnType<{ A, B }> // <- do this without specifying every generic? Here's one real-world example where this comes in very handy with OG generic bags. I'd love for this feature to cover this. 4) Declaring Generic BagsWith current generic bag workarounds, it's possible to declare the bag itself using a standalone type: type MyGenericsBag = {
A: MyType;
B: Omit<MyGenericsBag['A'], 'myField'>;
} Then this generics bag can be used as a generic on any function/class/etc. When lots of generics are involved, it would be tedious to have to declare the same generics bag in many places, so I'd rank this pretty highly on my wishlist. However, I feel like this one would be very difficult to support. I can't really think of a syntax without introducing a new keyword like: declare function fnA<G typemap MyGenericsBag>(a: G.A, b: G.B): void But then I feel this loses some of the elegance of the proposed syntax. Though this would also solve #3. 5) Accepting PartialsI'd expect a function to be able to specify partial generics and have the types of all relevant values fully inferred: class MyClass<{ A extends number; B extends number }> {
constructor(public a: A, public b: B) {}
}
const instanceA = new MyClass<{ A: 1, B: 2 }>(1, 2)
const instanceB = new MyClass<{ A: 3, B: 4 }>(3, 4)
function passMeInstances<T extends MyClass<{ A: 1 }>>(instance: T) {
instance.a // TS should know this is `1`
instance.b // wasn't specified, so defaults to `number`
}
passMeInstances(instanceA) // good
passMeInstances(instanceB) // error - A: 3 not assignable to A: 1 This is one aspect where current generics bag workarounds fall a little short. While the above example would work, TS fails to infer more complex types like tuples with specific shapes. To make it work, you have to add a cast inside the function body. You can see that on this Zedux doc page (see the comment that says "Current TS shortcoming"). I'd love for this feature to make such inference much more robust. SummaryI'm a big fan of this idea. I really hope it becomes a thing. Maybe I'm asking too much, but I'm sure I'm not alone in wanting this feature to be powerful enough that it completely replaces the current generics bag workarounds we have to do - which are pretty powerful already, so it would take a powerful feature to replace them. |
I love this idea, something I've wanted since default generic types existed About the syntax, I don't think it should move forward with the proposed one as it's not just vague, it can lead to several pitfalls: Modifying the function signature would lead to silent "reinterpretation" of usages of the default values. As an example, imagine that the signature of the function // Before
declare function fn<{ T extends number; T2 extends string; }>(a: T, b: T2): void
// After
declare function fn<T, T2 = string>(a: T, b: T2): void Then this existing usage of the function would change meaning fn<{ T: 100, T2: 'foo' }>(100, 'foo') Before it meant that My proposal would be to follow an python-esque syntax for named arguments fn<T = 100, T2 = 'foo'>(100, 'foo') If we compare that to the example before, it would fix the ambiguity when changing the function's signature |
I feel like this one is covered already by the proposal. Those bindings at the declaration site would work the same way as the current type parameters.
Your example is not about what I call a partial inference (I think?). This particular example would just pick up a default for
Right. I was thinking about this as well but I'm not sure what would be the right syntax for this. We'd need a way to both support the name for the bag itself and for its elements. Something like this could work: declare function fnA<TBag: { A extends MyType; B extends Omit<A, 'myField'> }>(
a: A,
b: B
): MyReturnType<TBag> but I'm not completely sure how that feels yet. It feels close to tuple labels - that is both good (familiarity) and bad (different semantics, labels are purely decorative~) at the same time.
From the call site's PoV, this would be capable of accepting an object type and I think that it's fine to accept just any structured type. As long as we can match the required "properties" then it should be fine.
That's likely depending on what is being discussed in relation to the upcoming |
Also, the current proposal only allows types for which named generics are possible to be defined in a specific way, i bet most developers would struggle to decide which syntax to use because as I understand the new syntax would restrict generics to only be passed as arguments when named, and positional generic arguments would be illegal and vice-versa |
That's syntactically less appealing to me but I also don't see this as a major problem. I could live with this. One caveat of this proposal is that it doesn't have a clean way of addressing some of the things mentioned by @bowheart (3 and 4) |
Cool, yeah I'm just clarifying most of these - making sure they aren't forgotten and if any are too much for the initial feature that we can make follow-up items.
Sorry, it was a bad example. I'm referring to #54047. I'm unsure how the partial inference sigil would work since it currently relies on order. I was trying to clarify whether we're expecting it to infer all other params automatically or if the call site would need to use a new syntax to specify which generics to infer. I assumed auto-infer since I'm always wary about introducing new syntax. But now that I think about it, I'm pivoting from that assumption. Here's a better example: function fnA<{ A = 1; B = 2; C = 3 }>(a: A, b: B, c: C): [A, B, C] {
return [a, b, c]
}
const nums = [10, 20, 30] as const
// are B and C inferred?
fnA<{ A: 10 }>(...nums) // [10, 20, 30]
// or defaulted
fnA<{ A: 10 }>(...nums) // [10, 2, 3] (would actually error since 20 is not assignable to 2)
// do we need a new syntax to specify which other generics to infer:
fnA<{ A: 10, B }>(...nums) // [10, 20, 3] - B inferred, C defaulted I like the python syntax (dare I mention PHP?). It may be wise to go that route since other languages have had success with it. But one thing I like about the "destructuring"-esque syntax is it feels like JavaScript. Passing generics and passing parameters have always felt similar, which helps with the learning curve. I'd love to keep that similarity. 3 is definitely low-priority. I'd much rather have a solid syntax if I had to choose. Without 4 though, the current generics bag workarounds would still be preferable in some situations. Maybe that's fine, but it's a tradeoff worth considering. |
A similar suggestion from long ago got abandoned because someone pointed out that object shape types get us halfway to named types: #23696 (comment) But as that issue pointed out, some kind of destructuring/defaulting would be nice, which is exactly why this proposal is so important. TS badly needs this; there's a reason so many languages end up implementing named function arguments, and why JS ended up implementing destructuring with defaults, and type parameters are no different. Dealing with more than two or three positional parameters is inevitably tedious; I've seen types with like 9 parameters when digging through code generated by Prisma, and it would be a lot easier to understand those types if there were named type parameters. @bowheart the difference in syntax between accessing Python named arguments and |
@Andarist TS already has tuple type spread and rest spread, so if you added tuple type defaults, object type spread and object type rest spread to the features in OP, it would bring object/array type declarations to parity with JS object/array value declarations, which would be supremely elegant. |
I agree with @eloytoro . It would be nice to have a new syntax which is compatible with existing TypeScript code. I doubt that the majority of library maintainers will rewrite their definitions just to allow for named parameters if the rewrite would be a breaking change. Example found in fastify: export interface FastifyReply<
RawServer extends RawServerBase = RawServerDefault,
RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
RouteGeneric extends RouteGenericInterface = RouteGenericInterface,
ContextConfig = ContextConfigDefault,
SchemaCompiler extends FastifySchema = FastifySchema,
TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
ReplyType extends FastifyReplyType = ResolveFastifyReplyType<TypeProvider, SchemaCompiler, RouteGeneric>
> { /*...*/ } It would be nice if one could use the type like: const handler = (request: any, reply: FastifyReply<ReplyType = MyEntity>) => {}; Instead, the currently best approach seems to be to write a fragile wrapper duplicating fastify's default types: type ReplyTypeFirst_FastifyReply<
ReplyType extends FastifyReplyType,
RawServer extends RawServerBase = RawServerDefault,
RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
RouteGeneric extends RouteGenericInterface = RouteGenericInterface,
ContextConfig = ContextConfigDefault,
SchemaCompiler extends FastifySchema = FastifySchema,
TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
> = FastifyReply<
RawServer,
RawRequest,
RawReply,
RouteGeneric,
ContextConfig,
SchemaCompiler,
TypeProvider,
ReplyType
>;
const handler = (request: any, reply: ReplyTypeFirst_FastifyReply<MyEntity>) => {}; (Disclaimer: The |
This feature is pretty useful! However, I would like to oppose the current proposed syntax, since it is ambiguous. Using Not a blocker! Just some suggestions. Since typescript itself has already introduced many non-js syntaxes to its Turing-complete type system, it won't be an issue if ts has introduced such python-like syntax to it. Any thoughts? |
Our library relies heavily on rich generic type parameters and this feature would be tremendously helpful to us. Anxiously watching this! |
Suggestion
π Search Terms
named type parameters generic bag
β Viability Checklist
My suggestion meets these guidelines:
β Suggestion
Disclaimer: I somewhat suspect that a similar feature request exists already but I couldn't find any.
I'd like to suggest an ability to specify named type parameters. Some libraries have long lists of generic type parameters (XState currently sits at 5: here) and it becomes hard for consumers to remember the order of those.
I think that it might be hard for me to beat @weswigham's arguments from here:
I think that it's best to model this feature after objects and destructuring patterns. That should create a familiar syntax for end users.
I'm not really married to any particular syntax and I think it's somewhere up for debate, one variation that comes to mind is this:
π Motivating Example
Any library with more than 2-3 type parameters. Vue, TanStack Query, XState and more come to mind.
Currently TanStack query has such an overload for its
useQuery
:This could likely be rewritten to something like:
A nice trait of this feature would be that generic names would become autocompletable. Today we might rely on signature help when typing but it doesn't provide the ideal experience. It has too much information (even if the interesting piece of information is in bold), it's squished into a single line and it's hard to focus on what we are typing (plus, of course, we can't selectively start typing those type parameters "out of order").
π» Use Cases
Currently one might use a "generic bag" to imitate this feature. Using the previous example this could look like:
One of the problems with this is that it's not easy to add defaults to specific slots, one has to resort to helper types like
WithDefaults
and implement that logic on their own.In fact, I currently have an open PR that would allow to infer
T
using such indexes like in the example above (this PR builds on top of an already referenced @weswigham's PR).My primary motivation is to enable such inference (based on indexes) for reverse mapped types. It's important for those to create multiple relationships within a single object property value/tuple element. It could be a nice addition for other type parameters that would make this feature more consistent. However, I think that when it comes to regular type parameters this approach has an important disadvantage when compared to this proposal.
Inferring using indexes doesn't provide the same capabilities as inferring to naked type parameters:
in
/out
/const
) for each "slot" (it's not possible to annotate part of the type parameter)I think that this featue is important to address those concerns and to enhance the flexibility for library authors.
When it comes to the implementation... I think that this could be added fairly easily. The main things that would have to be added to add support for this would be the changes in the parser and code "matching" the names with the parameters list. The parameters lists should still be kept internally as a flat array and matching would be implemented only on boundaries.
The text was updated successfully, but these errors were encountered: