Eliminate non-generic functions
See original GitHub issueEvery non-generic function can be expressed as generic function with a single type parameter corresponding to each argument. Explicit parameter type annotations simply correspond to extends
constraints on the type parameters of the generic equivalent.
While the title of the issue is “eliminate non-generic functions”, in practice this simply gets rid of all the syntax ceremony involved in declaring generic functions. A function declaration as follows:
function repeat(item, n: number) => Array(n).fill(item)
will be inferred as: type F = <T1 extends any, T2 extends number>(item: T1, number: T2) => T1[]
, and the result of invoking it with repeat("foo", 10)
is inferred as string[]
, not any[]
.
Issue Analytics
- State:
- Created 6 years ago
- Reactions:2
- Comments:28 (26 by maintainers)
Top Results From Across the Web
Tools for Managing Generic Functions - R
Remove all the methods for the generic function of this name. In addition, removeGeneric removes the function itself; removeMethods restores the non-generic ......
Read more >Tools for Managing Generic Functions - R
The functions documented here manage collections of methods associated with a generic function, as well as providing information about the generic functions ......
Read more >C# Generic & Non-generic Collections - TutorialsTeacher
It eliminates duplicate elements. Non-generic Collections. Non-generic Collections, Usage. ArrayList, ArrayList stores objects of any type ...
Read more >Why aren't Java Collections remove methods generic?
Remove is not a generic method so that existing code using a non-generic collection will still compile and still have ...
Read more >samirmenon / scl-manips-v2 / issues / #161 - Update CTaskBase ...
Update CTaskBase (remove non-generic functions). Issue #161 new. Samir Menon repo owner created an issue 2015-02-12. This code seems superfluous.
Read more >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Reopening
A compromise suggested by @simonbuchan seems to me like a great backward-compatible way to implement this, even though I do see the point of the syntax/semantics change that @masaeedu proposes.
So, here I go, coming up with my take on two proposals for this.
As I understand, motivation for implementing this feature is very simple:
Give developers a more simple and a less verbose way of defining generic functions, which are superior to regular functions in most use-cases. This allows for more user-friendly generics, which are easier to read, edit, and reason about.
Both proposals do not tackle default generic parameters in any way, so their syntax and usage are to remain the same as of now.
What is tackled, however, is a solution to #17445.
But, IMHO, the error presented in #17445 from the very start should’ve beed a warning instead.
This allows to still inform the user that they might be doing something fishy, while also allowing a perfectly valid operation.
# 1 - Implicitly-generic function parameters
This is, basically, what @masaeedu suggests, if I understand the issue correctly.
This proposal, however, needs evaluation in terms of backward-compatibility, as I’m not sure that it is actually backward-compatible.
Functions
All functions in TS are now generic by default and infer their parameters implicitly.
Function definitions like
are the same as
in current TS.
The short equivalent produces types with the same names as the parameters they correspond to. Another addition of this would be that the parameters can now “be referenced” as types, because the desugared type names are the exact same as the parameter names.
Writing “classic” generics explicitly, simply allows for a finer control of the function declaration by extracting repeating types from the definition into type variables.
For example, in this scenario:
the declaration sure does get very tedious to read.
So we need a way to extract the repeated type into a “type variable” of sorts, just like how we would do in current TS:
which would be equivalent to writing this in current TypeScript:
This ensures that all functions are treated the same way, and current generic syntax becomes basically a place for defining type variables for functions.
Most generics in type-heavy code (properly typed code) are used for this exact purpose - declaring type variables for later use in conditionals and stuff.
So, I’d say that actually the semantics of generics do change.
But I can only see positive effects in this.
Classes
Classes and functions in JavaScript and TypeScript are used nearly interchangeably by many developers, which makes the possible scope of this issue wider than just functions.
So, if it does touch classes, then this section becomes relevant.
Using this syntax in classes is a big concern, as it looks like it would require a big rework of how the class generics are currently handled.
With this syntax in mind, constructors now must have generics in order to satisfy the monomorphic conversion:
Such a constructor would not affect the instantiation of the class, and use its generic parameters only as type variables:
Inferring constructor parameters would work just as it does now:
Conclusion
I’d say that it changes dramatically how TypeScript code is perceived by your average developer and is potentially a source for breaking changes, as TS has notable issues with how it currently handles generic parameters (like #14400), which seem like they need to be resolved first in order to implement this proposal correctly.
Also, developers might get confused on what exactly a generic parameter now is, while also having no way to force good-old regular parameters if they need them for some reason.
Oh, and I also can’t imagine any workarounds for avoiding constant #17445 here.
# 2 - Explicitly-generic function parameters with syntax sugar
This is, basically, what @simonbuchan suggests.
Contrary to the previous one, this proposal doesn’t change the semantics of generics, but rather the syntax of parameter’s type definitions.
Functions
All functions in TS stay the same, nothing changes, full backward-compatibility.
However, the
extends
keyword is now allowed after any function parameter andinfer
is allowed afterextends
.So, function definitions like
are just a syntax sugar for
The short equivalent produces types with the same names as the parameters they correspond to.
This proposal allows for very flexible and easy-to-read generic function definitions:
Another addition of such syntax would be that the parameters can now “be referenced” as types, because the desugared type names are the exact same as the parameter names:
The example is a stretch, but it gets the point across, I guess.
One should actually write functions like these like this:
The addition of
infer
into the mix is not necessary, but makes the new syntax much more useful… and just so gorgeous:This is what’s supposed to happen when a compiler encounters
infer
after theextends
in parameters:extends
clause into generic parameters with the same type name as the parameter name, while replacing it with: %parameterName%
in function parameters.declare function map<T, f extends (value: T) => infer R>(array: T[], f: f): R[];
infer %
withany
in the extracted clause.declare function map<T, f extends (value: T) => any>(array: T[], f: f): R[];
%
frominfer %
, where%
is a placeholder for the parameter name.declare function map<T, f extends (value: T) => any, R>(array: T[], f: f): R[];
extends
after the type parameter name.declare function map<T, f extends (value: T) => any, R extends >(array: T[], f: f): R[];
extends
.declare function map<T, f extends (value: T) => any, R extends f extends (value: T) => infer R>(array: T[], f: f): R[];
? % : never
, where%
is a placeholder for theinfer %
parameter name.declare function map<T, f extends (value: T) => any, R extends f extends (value: T) => infer R ? R : never>(array: T[], f: f): R[];
any
.So, the
infer
keyword in parameters is a shorthand for conditional types in the same way asextends
is a shorthand for generic types.And in case a more complicated conditioning is needed, it’s always possible to revert back to using “classic” generic syntax.
However, old typescript definitions can’t take advantage of this, as no new semantics for existing syntax are introduced.
But, as a nice bonus, this proposal brings zero breaking changes!
Classes
Classes and functions in JavaScript and TypeScript are used nearly interchangeably by many developers, which makes the possible scope of this issue wider than just functions.
So, if it does touch classes, then this section becomes relevant.
extends
in constructor parameters is disallowed just like generics, but are still allowed in methods and properties.So, no breaking changes here either.
Conclusion
My personal favourite is this one, as it brings no breaking changes, while also allowing developers to write generic functions much more easily, greatly increasing the probability that a developer would prefer to write a nice generic function:
instead of this:
as practically no extra code is introduced in the process,
making this proposal’s syntax greatly superior to simple
param: type
definitions.Adding to this is the fact that generic function parameters and regular function parameters are still very distinctive, which makes the new syntax much easier to adopt.
#17445 can be simply worked around by applying the extended type consequently, i.e. like this:
The case that @mhegazy mentioned
I agree with everything but the “for users” part.
TypeScript’s learning curve seems to me to be quite linear anyway, even with all the syntactic load that only adds to the learning curve of JavaScript.
Even if we were to go off of examples here, there’s another bit of syntactic sugar in TypeScript, to which the argument above applies just as well - parameter properties. They add a new, simple syntax for an existing functionality, just like the second proposal does. It’s less verbose and more “to the point”.
So if it’s allowed to exist, then why something like this shouldn’t be also?
I mean, a tiny bit of syntax sugar over generics won’t magically turn TypeScript into Scala… 😅
And when it comes to maintenance, current generics syntax is not that maintainable in the first place: it’s not uncommon to see even relatively small functions’ declarations spanning over multiple lines just because the generic parameters for them would span over the whole screen otherwise and become practically unreadable.
And I’m still yet to mention what happens when people try to give meaningful readable names to generic parameters, instead of reciting the alphabet…
Warning: typical production code imminent...
I tried to represent as many syntactic variations as I could while also keeping the amount of text reasonably small.
I’m not very strong in writing EBNF definitions, so none are present, please, pardon me here.
Any questions/additions are welcome as it’s my first try ever on writing a proposal here, and I’m willing to continue on completing these proposals’ definitions.
As for the final implementation - I’d be happy if any of the two makes it.
Also, both proposals make possible the case mentioned here, just in slightly different ways.