question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Eliminate non-generic functions

See original GitHub issue

Every 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:open
  • Created 6 years ago
  • Reactions:2
  • Comments:28 (26 by maintainers)

github_iconTop GitHub Comments

4reactions
RyanCavanaughcommented, Sep 25, 2017

Reopening

2reactions
Raiondesucommented, Sep 24, 2020

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

declare function add(x: number, y: number): number;
declare function oneOfThree(x, y, z): x | y | z;

are the same as

declare function add<x extends number, y extends number>(x: x, y: y): number;
declare function oneOfThree<x, y, z>(x: x, y: y, z: z): x | y | z;

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:

declare function addObjectKeys(
  obj1: { [key: string]: number },
  obj2: { [key: string]: number },
  key: keyof typeof obj1 & keyof typeof obj2
): number;

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:

declare function addObjectKeys<O extends { [key: string]: number }>(
  obj1: O,
  obj2: O,
  key: keyof O
): number;

which would be equivalent to writing this in current TypeScript:

declare function addObjectKeys<
  O extends { [key: string]: number },
  obj1 extends O,
  obj2 extends O,
  key extends keyof O
>(
  obj1: obj1,
  obj2: obj2,
  key: key
): number;

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:

// This:
class Box<T> {
  constructor(public param: T) {}
}

// Should now mean this:
class Box<T> {
  constructor<param extends T>(param: param) {}
}

Such a constructor would not affect the instantiation of the class, and use its generic parameters only as type variables:

new Box<'foo' | 'bar'>('foo');
// Generic `constructor` has no effect on the final constructor function invocation

Inferring constructor parameters would work just as it does now:

// This:
class Box<T> {
  constructor(public param: T) {}
}
new Box('foo'); // Box<string>

// Should now mean this:
class Box<T> {
  constructor<param extends T>(param: param) {}
}
new Box('foo'); // Box<string>

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 and infer is allowed after extends.
So, function definitions like

declare function add(x extends number, y extends number): number;
declare function keys(obj extends object): Array<keyof obj>;

are just a syntax sugar for

declare function add<x extends number, y extends number>(x: x, y: y): number;
declare function keys<O extends object>(obj: O): Array<keyof O>;

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:

type Foo = { foo: number };

// this beauty:
declare function addToFoo(foo extends Foo, x: number): number;

// instead of this pile of boilerplate code:
declare function addToFoo<foo extends Foo>(foo: foo, x: number): number;

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:

// actually the generic parameter `f` is referenced in the return type; it just has the same name as the function parameter, which makes the whole deal easier to read
declare function map<T>(array: Array<T>, f extends (value: T) => any): Array<ReturnType<f>>;
// equivalent to:
declare function map<T, f extends (value: T) => any>(array: Array<T>, f: f): Array<ReturnType<f>>;
The example is a stretch, but it gets the point across, I guess.

One should actually write functions like these like this:

declare function map<T, R>(array: Array<T>, f: (value: T) => R): Array<R>

The addition of infer into the mix is not necessary, but makes the new syntax much more useful… and just so gorgeous:

// even better so:
declare function map<T>(array: T[], f extends (value: T) => infer R): R[];
// equivalent to this mouthful:
declare function map<
 T,
 f extends (value: T) => any,
 R extends f extends (value: T) => infer R ? R : never
>(array: array, f: f): R[];

This is what’s supposed to happen when a compiler encounters infer after the extends in parameters:

  1. Extract the 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[];
  2. Replace infer % with any in the extracted clause. declare function map<T, f extends (value: T) => any>(array: T[], f: f): R[];
  3. Add another type parameter definition right after the extracted clause, name new parameter as % from infer %, where % is a placeholder for the parameter name. declare function map<T, f extends (value: T) => any, R>(array: T[], f: f): R[];
  4. Make the new parameter infer a value from the original clause via a conditional type.
    1. Add extends after the type parameter name. declare function map<T, f extends (value: T) => any, R extends >(array: T[], f: f): R[];
    2. Copy the original extends clause with the original parameter name after the previously added extends. declare function map<T, f extends (value: T) => any, R extends f extends (value: T) => infer R>(array: T[], f: f): R[];
    3. Add ? % : never, where % is a placeholder for the infer % 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[];
  5. In case of unsuccessful result of these steps, resolve the type to any.

So, the infer keyword in parameters is a shorthand for conditional types in the same way as extends 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.

// This should produce a compiler error!
class Box<T> {
  constructor(
				// here
	public param extends T
  ) {}
}

// Because it's just syntax sugar for
class Box<T> {
  constructor<param extends T>(public param: param) {}
}
// which is illegal
class Box<T> {
  constructor(public value: T) {}

  map(f extends (value: T) => infer R): Box<R>;
}
// equivalent to:
class Box<T> {
  constructor(public value: T) {}

  map<f extends (value: T) => any, R extends f extends (value: T) => infer R ? R : never>(f: f): Box<R>;
}

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:

declare function someFunction(param extends any): param;
// desugared to:
// declare function someFunction<param>(param: param): param;

instead of this:

declare function someFunction(param: any);

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:

declare function equal(x extends string, y: x): boolean;
// equivalent to:
declare function equal<x extends string>(x: x, y: x): boolean;
// which is also equivalent to the classic way:
declare function equal<T extends string>(x: T, y: T): boolean;
// making the types assignable as per the conclusion in #17445

///
declare function concat(x extends string, y: x, z: x): string;
// equivalent to:
declare function concat<x extends string>(x: x, y: x, z: x): string;
The case that @mhegazy mentioned

Having a new syntax to define generic type parameters does not seem to be the right solution either. adding new constructs/syntax to the language increases learning and maintenance costs for both users and compiler maintainers.

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...

image


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.

Read more comments on GitHub >

github_iconTop 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 >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found