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

Describe function signature using an interface #34319

Open
5 tasks done
fabiancook opened this issue Oct 14, 2019 · 13 comments
Open
5 tasks done

Describe function signature using an interface #34319

fabiancook opened this issue Oct 14, 2019 · 13 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@fabiancook
Copy link
Contributor

fabiancook commented Oct 14, 2019

Search Terms

function implements interface

Suggestion

Allow functions to indicate interfaces that they implement, allowing usage of a function interface.

Properties defined on an interface would not be possible unless they are defined as optional

Use Cases

When using function overloads, the return type is defined as any, this would allow the return type to be guarded and not use any. Referencing: https://www.typescriptlang.org/docs/handbook/functions.html#overloads

It would also allow optional properties to be defined on a function without doing type gymnastics.

Examples

interface CoolFunction {
  (first: string, second: number): number;
  (first: number, second: string): string;
  property?: boolean;
}

// args will be of type   [string, number] | [number, string]
function coolFunction(...args) implements CoolFunction {
  if (typeof args[0] === "string") {
    // We're now reduced our type of args to [string, number]
    return args[1];
  } else {
    // args can only be [number, string]
    return args[1];
  }
}

// This property is known as it is defined on the interface
coolFunction.property = true;

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.
@fabiancook
Copy link
Contributor Author

fabiancook commented Oct 14, 2019

This also allows developers to define interfaces to design functions, without implementing them (meaning no empty un-implemented function blocks).

The developer could then use interfaces within tests or code before the implementation is available.

Right now I am defining interfaces and functions separately (but with same constraints), letting me design the constraints and having one of our developers who is new to typescript (and javascript) fill in the implementation. The implementation is then passed to a test that accepts a function with the interface's signature.

function name(first, second) implements Interface {} seems natural as well, and not jarring (maybe it is to some? Is there an alternative?), and also aligns with the usage with class

Using this pattern also means the argument types or return type doesn't need to be defined, as the interface has already done this (this wasn't mentioned in the above issue, but is in the example)


I do have a concern though that implements might not be the correct term here, but it also seems right.


An example of type gymnastics being resolved:

Function type interface:

interface SearchFunc {
    (source: string, subString: string): boolean;
}

Current:

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    let result = source.search(subString);
    return result > -1;
}

Proposed:

function mySearch(source, subString) implements SearchFunc {
    let result = source.search(subString);
    return result > -1;
}

Current Source: https://www.typescriptlang.org/docs/handbook/izterfaces.html#function-types

@fabiancook
Copy link
Contributor Author

fabiancook commented Oct 14, 2019

This would also allow this pattern:

interface Dispatcher<T> {
  (value: T): boolean;
}

function dispatch<T>(value: T, fn?: Dispatcher<T>) implements Dispatcher<T> {
  if (fn) {
    return fn(value);
  }
  // Do something default
  return false;
}

The alternative would be duplicating the types:

function dispatch<T>(value: T, fn?: Dispatcher<T>): boolean {
  if (fn) {
    return fn(value);
  }
  // Do something default
  return false;
}

It may not be significant, but it reduces the surface area of where types are defined (e.g. I can change the return type on the interface and it would also change the return type of the function), which in my opinion is a bonus. In the example I have just used boolean, but the return type could be a lot more involved (e.g. when we get into some complicated generics)

This sneaked into this comment's examples as well, but any arguments not defined in the argument list of the interface would need to be optional, else the function couldn't match the defined interface, the alternative would be not to allow additional arguments, but this is avoidable

@ilogico
Copy link

ilogico commented Oct 15, 2019

Regardless of the syntax (which I think is quite OK), it solves the problem of not being able to make function declarations implement anything, so now I have to write const MyTemplate: Template<Prop> = (props) => ...

One thing to bear in mind is that the type of the arguments can only be optional if the interface/interfaces declare one call signature. I think it's reasonable the function implements on or more interfaces without call signatures. With multiple call signatures, it becomes an overloaded function and should declare compatible arguments (which I believe is the current rule for implementing overloaded functions) .

Another issue is the hoisting and TDZ for the properties/methods. It would be great if we could preserve the current behaviour.

interface Something {
  value: number;
}

something.value; // should be illegal
something.value = 42; // should be OK
something.value; // should be OK
function something() implements Something {}

@fabiancook
Copy link
Contributor Author

fabiancook commented Oct 15, 2019

to make function declarations implement anything

That is a great point, you aren't able to provide their complete type ahead of time using current syntax because you cannot define properties.

TypeScript is then maybe breaking the goal of Preserve runtime behavior of all JavaScript code. without something similar to this

As with your example, it would work the same as let:

let value: number;

console.log(value); // should be illegal
value = 42; // should be OK
console.log(value); // should be OK

@ahejlsberg ahejlsberg added the Suggestion An idea for TypeScript label Oct 15, 2019
@gitowiec
Copy link

This could help me implement function property (like here https://www.bennadel.com/blog/3250-defining-functions-with-properties-using-typescript-declaration-merging-in-angular-2-4-9.htm) when using React.FunctionComponent.

@JacobWeisenburger
Copy link

Did this functionality ever get added to typescript? Or is there some kind of work around? I couldn't tell if there was a conclusion that was reached on this topic.

@eddiecooro
Copy link

eddiecooro commented May 3, 2020

I came across this problem back a while ago and after a little research I found this thread.
I want to add that, this issue is super important when it comes to typing React components that have a generic type. because in that situation, there is no way to type the component using the arrow function + React.FC interface:

const MyComponent: FC<Props<T>> = <T>(props) => { ... }

(in the above example, we don't have access to the function generic type at the point of declaring the MyComponent Type so the FC<Props<T>> errors)

So we have to use the function form, but by doing that, we won't be able to use the FC interface anymore so we have to do this:

function <T>MyComponent(props: Parameters<FC<Props<T>>>[0]): ReturnType<FC<Props<T>>> { ... }

It isn't that beautiful, but it wouldn't matter if it worked. even with this syntax, we won't get proper type check for things like Component.propTypes and Component.defaultProps. So we also have to add the definitions manually for each one of them (we can't use the alias declaration trick in this case because that way there is no way to get the generic type).

So after all, it would be great if typescript had first-class support for this kind of situation. I'd love to be able to do something like this:

function <T>MyComponent(props) implements FC<Props<T>> {
  ...
}

@JacobWeisenburger
Copy link

JacobWeisenburger commented Jul 10, 2020

is there some kind of work around for until this feature gets implemented?

@lifeiscontent
Copy link

@RyanCavanaugh any status update on this one?

@JacobWeisenburger
Copy link

@RyanCavanaugh Can we get something like this?

@RyanCavanaugh
Copy link
Member

Hey folks, we've got a few thousand open suggestions to manage, and pings for "updates" are no-ops because we'll always post updates in-thread. Thanks!

@fabiancook
Copy link
Contributor Author

In addition to the above given syntax, if we completely forgot about the class syntax existing, and only utilised function + implements, then gave a type for prototype by utilising new:

interface CoolPrototype {
  foo(): boolean
}

interface CoolConstructor {
  new (): CoolPrototype
  (): CoolPrototype
}

function coolConstructor() implements CoolConstructor {
  if (!(this instanceof coolConstructor)) return new coolConstructor()
  return this 
}
coolConstructor.prototype = {
  foo() {
    return true
  }
}

By dropping the TypeScript stuff, this works as I would expect, matching the types defined...

image

I originally mentioned the property to be optional, but by having it required, the type checker would/could need to see if there is assignment to that property (what happens if I wanted to do this.foo = () => true instead?)

Here is also a playground link with how you can achieve the same (constructor + normal function) with a type, with usage of as in there

https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgMIHt0BsAKV1gECeADigN4BQyyMmAFAJQBcyARplhHCJQL6VKoSLEQoM2DCADOYKAFcEhKMio0QEAO7ImrCbnyEwpCNR0s0nPAWJkzJQ7Yh6rj43YGUE6GWGQObF2xrIxNkAF5VMxo6dCYomkTEqAgweSgQZDl5UySBTxh5ECVgH2RvSR9ZBSV0KHowAAtgaSCDG3cUAB9kIoBrEHRNEAt9EKcE5GAYHQBCBubpKd8eJHQZiqwpasVlRkZkFLSM5A1tek3t7NqVOCX9K5q9pjMzAHo3y3ksABNkH-QWUWy1k3B+AH53p8mi0AHSxCLmCIAPlocCw0lMZiO6UyMKW-EElyq12UsICoTIiIp6CJJPKnEeuzqGBIRDaTJuiOJviedWQdx0nXWDMqvOZKgAZJN1FpzG1xp1+Iw6b5RVgFW4wpEeTsbqyiC9vGrNgAmTUdbWnOW60ks9BsphAA

This would also allow us to do to this...

let cached: CoolPrototype
function coolConstructor() implements CoolConstructor  {
  if (cached) return cached
  if (!(this instanceof coolConstructor)) return new coolConstructor(...arguments)
  cached = this
  return this
}

coolConstructor.prototype = {
  foo() {
    return true
  }
}

console.log(coolConstructor() === coolConstructor())
console.log(coolConstructor() === new coolConstructor())

image


Of course we could just remember the class syntax exists and instead use it, but, this could allow interface + implements to be written for code bases currently using this style of function

@rickdgray
Copy link

rickdgray commented Aug 5, 2021

It is so frustrating to me that I can define an interface for a function, and then it not get enforced. Doesn't that completely defeat the point of an interface? If I can just assign anything to a function with an interface, I might as well not have the interface at all.
image
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

9 participants