microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.34k stars 12.54k forks source link

Automatic function signatures #22185

Closed vitaly-t closed 6 years ago

vitaly-t commented 6 years ago

This is a feature request / proposal.

To add support for automatic function/method signatures by reference.


PROBLEM

When we have an existing function or method that we want to override or re-implement in another class or interface, we re-declare the complete set of parameters that the original function takes, plus the return type.

When the function signature is simple, we can just copy and paste the parameters signature. But sometimes the parameters signature can be very complex. And the worst-case scenario is when parameters include use of templates, other function signatures, and many other nested types.

In this case re-declaring the function signature can become a nightmare, as one needs to import a lot of extra types to declare a function with the same signature. The same goes for the return types.

SOLUTION

Add the ability to declare a function that exactly matches the signature of an existing function, for both parameters and the return type.

class A {
    method1(/* long list of complex input types*/) : /* complex return type */
    { 
    }

    method2: method1 // repeats the method signature precisely
    { 
        // expects the same parameters and the return as for method1
    }
}

class B extends A {
    method3: A.method1 // repeats the method signature precisely
    { 
        // expects the same parameters and the return as for A.method1
    }   
}

The same for regular functions:

function func1(/* long list of complex input types*/) : /* complex return type */
{
}

function func2 :func1
{ 
    // expects the same parameters and the return as for func1
}

i.e. when the type is an existing function/method, automatically apply the exactly same parameters+return signature.

The type reference should be fully reusable, i.e. available equally across classes, interfaces and simple functions.


In addition, such re-declared functions should support an optional list of argument names, for the sake of explicit local visibility:

function func1(first: text, second: boolean, third: DateTime) : Promise<any>
{
}

function func2(first, second, third) :func1
{ 
   // because the function type is of an existing function, the type of all parameters
   // and the return type are automatically inferred from the reference type.
}

Another beauty of this addition is to be able to declare only the parameter names you are interested in, while their order is irrelevant:

function func2(third, one) :func1
{ 
   // this function will use only parameters `three` and `one` from the original signature
}

This feature will make a lot of code shorter, and save great deal of time by not repeating existing function signatures over and over again.

ghost commented 6 years ago

Duplicate of #22063, though in that case you would have to write typeof otherFunction, and the parameter list would still be required. It would be out of scope to inject parameter names into code based on types, see the design goals.

vitaly-t commented 6 years ago

@andy-ms I see it being related, but hardly a duplicate, for the way I describe the problem and the solution.

ghost commented 6 years ago

The particular solution you described would be out of scope due to injecting parameter names based on the type provided, thus allowing types to change the meaning of code. Also, function func2(third, one) :func1 is already valid syntax (assuming func1 is the name of a type). The other proposal avoids type-directed emit and avoids changing the meaning of existing syntax.

vitaly-t commented 6 years ago

...allowing types to change the meaning of code.

I'm not sure I'm following. When we declare the parameter names, they expected to match names within the reference function type, or else there would be an error.

And because their type is pre-defined within the reference function type, we can declare them locally in any order, as it is no longer relevant.

But none of the above changes the meaning of the code or types. You would still maintain the exact same function signature.

Also, function func2(third, one) :func1 is already valid syntax (assuming func1 is the name of a type).

In the proposed solution, the type is an existing function/method, so it would apply the signature for parameters + return type. That's quite a different thing. And TypeScript could easily tell the difference, as function/methods are currently not used as types.

So, if you declare the type as an existing function/method - you are saying I want its exact declaration signature.

ghost commented 6 years ago

And because their type is pre-defined within the reference function type, we can declare them locally in any order, as it is no longer relevant.

If the TypeScript compiler just strips the : func1 annotation, that would cause errors at runtime, because in JS the parameter order does matter. It sounds like you expect the compiler to re-order the parameters in the compiled JS based on the type of the function. And for the example with no parameters listed: method2: method1 { ... }, this isn't valid JS if we just strip out the types, giving us method2 { } which is invalid syntax. The proposal requires us to fill in a parameter list based on the type of method1. If the compiler does more work than just stripping out the annotation, then it requires the type to affect the emit, something we've avoided.

vitaly-t commented 6 years ago

It sounds like you expect the compiler to re-order the parameters in the compiled JS based on the type of the function.

Yes, but the compiler wouldn't care, as it would always re-apply the complete exact signature in the right order. It would be just the TypeScript convenience being able to declare only the parameters that you do intend to use, without affecting what is generated into JavaScript.

If this is still somehow an issue, which I do not see, we could have the new function force re-declare all the parameter names in the right order.

For example, if you need a function with a complex signature that acts as a dummy (has empty implementation), then you wouldn't want to re-declare any of the parameters at all. A typical example of it would be when stubbing interfaces within tests.

ghost commented 6 years ago

Based on the design goals we can't do any generation of parameters, because it would involve the type of the function changing the emitted code.

Note that it's already possible to omit parameters at the end that you aren't using:

type Fn = (x: number) => void;
const fn: Fn = () => {};

But we can't allow you to omit parameters when you do use them, because that wouldn't be valid JS. But we could allow you to omit their types:

function f: Fn(x) {
    // 'x' is typed as 'number'
    x;
}

which allows you to omit the type on x given that the function already has a type. But then I think that is the same issue as #22063.

vitaly-t commented 6 years ago

Based on the design goals we can't do any generation of parameters, because it would involve the type of the function changing the emitted code.

As I wrote above...

the compiler wouldn't care, as it would always re-apply the complete exact signature in the right order. It would be just the TypeScript convenience being able to declare only the parameters that you do intend to use, without affecting what is generated into JavaScript.

So the emitted code would not be affected.

ghost commented 6 years ago

Could you provide a complete example of your revised proposal?

vitaly-t commented 6 years ago
function func1(first: text, second: boolean, third: DateTime) : Promise<any>
{
}

function func2(second) :func1
{ 
   // this implementation uses only parameter `second` from the signature function,
   // and the return type is automatically inferred from the reference type.
}

The TypeScipt compiler will automatically generate JavaScript from this:

function func2(first: text, second: boolean, third: DateTime) : Promise<any>
{
}

as it doesn't care about the function skipping parameters or declaring them in a different order, it only cares about their names, they are normalized automatically, according to the signature function, before generating JavaScript.

The same goes for classes and interfaces.

The following will throw an error:

function func2(fourth) :func1
{ 
}

because there is no parameter fourth in the signature function.

ghost commented 6 years ago

The TypeScipt compiler will automatically generate JavaScript from this:

That's exactly what I was talking about -- it's out of scope for us to automatically generate JavaScript, besides stripping types (and transpiling esnext features). In what you're describing, the emit would be different if the type of func1 were different, so it would be a type-directed emit.

vitaly-t commented 6 years ago

it would be a type-directed emit.

Yes, the emit would always be based on the reference type, doesn't matter if you declare or not parameters locally, it won't change the output.

RyanCavanaugh commented 6 years ago

Yes, the emit would always be based on the reference type

Just to re-iterate: We don't change the emitted code based on resolved types. There is an explicit "we do not do features like this" design goal around this.

typescript-bot commented 6 years ago

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.