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

Declare a function as a type; or, defer resolving generic types when declaring a variable as a type #29613

Closed
5 tasks done
MattiasMartens opened this issue Jan 28, 2019 · 3 comments
Labels
Duplicate An existing issue was already created

Comments

@MattiasMartens
Copy link

MattiasMartens commented Jan 28, 2019

Search Terms

  • functions
  • higher-order functions
  • generic types
  • type inference
  • type parameters
  • infer

Suggestion

This comprises two suggestions in one as I want to leave it up you to decide which is better value-for-effort. The suggestions are also compatible with each other.

1: typing a function

When declaring a function, I want to be able to declare the type of the function as a whole, not just the type of the parameters or the return value. I want this to be compatible with generic types just like the existing function declaration syntax is.

Example:

type MyStupidType<A, B> = (arg: A, transform: (sameArg: A) => B) => B

function myStupidFunction(arg, transform) {
  return transform(arg);
}: MyStupidType<A, B>;

The above is idiomatic, but clunky, so an alternative suggested syntax is:

<MyStupidType<A, B>> function myStupidFunction(arg, transform) {
  return transform(arg);
}

Since the function may not even have to reference the generic types explicitly (as in the example), it may even be possible to drop them, like so:

<MyStupidType> function myStupidFunction(arg, transform) {
  return transform(arg);
}

Or like so:

<MyStupidType<>> function myStupidFunction(arg, transform) {
  return transform(arg);
}

Or like so:

<MyStupidType<?, ?>> function myStupidFunction(arg, transform) {
  return transform(arg);
}

2: typing a const generically

The usual workaround for the problem I'm facing is to use arrow functions instead:

const myStupidFunction: MyStupidType<any /* ;( */, (sameArg: any) => any /* ;( */> = (arg, transform) => transform(arg)

As you can see, this doesn't work with generics, because I have to make the function non-generic in order to declare it in this way.

The proposed change is to allow the const to remain generic using the infer keyword:

const myStupidFunction: MyStupidType<infer A, infer B> = (arg, transform) => transform(arg);

...or, as suggested in 1), just dropping them if they do not need to be directly referenced:

const myStupidFunction: MyStupidType = (arg, transform) => transform(arg);

Use Cases

I ran into the problem described in #9366 while playing with types describing reducers (in an attempt to building up to understanding transducers):

type SimpleReducer<T, V> = (acc: V, next: T) => V;

type AbstractReduce<T, V> = (
  iterable: Iterable<T>,
  reducer: SimpleReducer<T, V>,
  initialValue: V,
) => V;

const reduceIterable: AbstractReduce = (
  iterable,
  reducer,
  initialValue,
) => {
  let out = initialValue;

  for (let next of iterable) {
    out = reducer(out, next);
  }

  return out;
};

This of course doesn't work because AbstractReduce is being referenced with no type parameters. But my implementation here is still too abstract for me to want to supply them.

However, this works:

function reduceIterableFn<T, V>(
  iterable: Iterable<T>,
  reducer: SimpleReducer<T, V>,
  initialValue: V
): V {
  let out = initialValue;

  for (let next of iterable) {
    out = reducer(out, next);
  }

  return out;
}

This would be fine except that it means I can't use my interface, because there's no way to tell TypeScript that a function implements a generic interface (as opposed to its parameters or return type considered individually).

The overall problem of deferring resolution of generics until a function is called turns out to be extremely difficult under the constraints of TypeScript; but what I'm proposing (hopefully) is not and it suffices for my use case.

Examples

The above sections show examples. When a developer writes a function that needs to implement a type, they can use the above syntax to allow TypeScript to extend the current behaviour around function declarations to function declarations that implement types.

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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh
Copy link
Member

Previously suggested in similar forms
#16918
#19106
#17574

I think this proposal tries to sort of do all of them at once and would be best tackled individually.


I can't say for sure that it's fatal, but the syntactic confusion problem here is overwhelming for the case where the function type is generic (ignoring the position of the annotation for the moment):

function myStupidFunction(arg, transform) {
  return transform(arg);
}: MyStupidType<A, B>;
// ... ? ...
function myStupidFunction(arg, transform) {
  return transform(arg);
}: MyStupidType<string, number>;

It's extremely unclear here whether the type arguments are introducing new types with those names, or referencing existing types by those names.

Similarly here:

const myStupidFunction: MyStupidType<infer A, infer B> = (arg, transform) => transform(arg);

My reading is that this is a non-generic function with two (implicit) any arguments.

Like other names, type arguments need to be introduced in an unambiguous syntax that makes it reasonably clear to the reader what's going on. Right now, in a type annotation position, T<U> is unambiguously a reference to U, not an introducing declaration.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Feb 5, 2019
@MattiasMartens
Copy link
Author

MattiasMartens commented Feb 5, 2019

@RyanCavanaugh Thanks for taking a look at this.

Having looked at the tickets you referenced, I do agree that #16918 and #19106 address the first part of the problem (whole-typed functions) and #17574 is a more thoroughgoing solution to the second part (deferred generics, which I believe is very similar to what @maseedu is calling "arbitrary parametric polymorphism").

Going over it now I do think I was mixing up some concerns. I'll split them up below to see if any of them are worth investigating.

Setting aside polymorphism completely, whole-typing functions is intuitive in the case of arrow functions:

type MyFunctionType = () => string;

const greenArrow: MyFunctionType = () => "green";

It can also be done with function declarations:

const greenFunction: MyFunctionType = function() {
  return "green";
}

But that's a pretty ugly syntax (and also not exactly equivalent because of hoisting). My favourite syntax of the ones I suggested is

<MyFunctionType> function greenFunction() { return "green"; }

I can propose that over on #16918 and dispense with it here.

Then there's arbitrary polymorphism. #17574 doesn't seem to propose a syntax, but it does seem like some kind of syntax would be necessary for some of the cases the issue is describing (e.g.

let id1 = undefined as Id1<any> // forced to supply type parameter here

—in the pie-in-the-sky scenario here, what would one use in place of <any>?). This is where I think the infer keyword might be useful to borrow.

However—and this is the core motivation for my suggestion—a thoroughgoing solution for arbitrary polymorphism looks a long way off (because of problems discovered investigating #9366, #9949, also see #24626; I believe this concern would affect any complete implementation of #16918 but please correct me if I am wrong), while a special syntax for whole-typing functions is comparatively easy. It just happens that in the particular case where we're typing a function declaration, this:

type MyGenericFunction<T>: (arg: T) => T;

<MyGenericFunction<infer T>> function myFunction(arg) { return arg; }

is a restatement of this already supported syntax:

function myFunction<T>(t: T) {
  return t;
}

With the added benefit of type-checking with a reusable interface.

My point is that if we can agree on

  1. The syntax for whole-typing of functions and
  2. The syntax for type parameters that are meant to be resolved only when something calls the generic (I believe this is a prerequisite of Supporting generic type inference over the other higher-order functions #9366)

Then we have an opportunity to get the benefits of arbitrary polymorphism on whole-typed functions without having to tackle the hard problems of solving the general case—because what I'm proposing is equivalent to what the engine is already doing.

@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

3 participants