Skip to content

Feature request: represent Function via generic type parameterized by its return/parameter/this type  #12342

Description

Background:

With #11929, we can get property type of an object. But we cannot get the return/parameter/this types of a function at compile time.

For single function argument, getting return/parameter type is relative simple by adding generic parameter. But when API is designed to accept a map of functions, it become hard to express in TypeScript.

Using function is a common pattern in vuejs and its derivation.

const component = new Vue({
  computed: {
    getNumber() {
       return 42
    }
  }
})

component.getNumber // 42

By combining mapped types and keyof, we can achieve some compile time types to capture this pattern.

declare interface Vue {
  new <T extends {[k: string]: Function}>(config: {computed: T}): { [K in keyof T]: ReturnType<T[K]>}
}

If only we have a special type called ReturnType.

Another usage is for event handling in most frontend framework. For example, backbone has an event-map syntax for event binding.

book.on({
  "change:author": authorPane.update,
  "change:title change:subtitle": titleView.update,
  "destroy": bookView.remove
});

book.trigger("change:author", author) 
// author should have the parameter type of authorPane.update

Also, with return/parameter/this type support, we can achieve typed bind/apply/call

interface FunctionTyped<ThisType, ArgTypes, RestArg> {
  bind: FunctionTyped<void, [ThisType, ...ArgTypes], RestArg]>
  apply: FunctionTyped<void, [ThisType, [...ArgTypes, ...RestArg]], never>
  call: FunctionTyped<void, [ThisType, ...ArgTypes], RestArg>
}

Note, this would require variadic generic types.

Proposal:

For every function type, we can have a special access key for return type/ parameter type. Parameter type is a tuple type for every argument. If function has rested parameter, parameter type will fallback to array type.

For overloaded function, the return type/parameter type is the union of all corresponding type of overloading signature.

For generic type parameter with function type constraint, the return type is resolved to constraint. the parameter type also resolves to constraint's parameter type (not regarding with variance.) If generic type has no constraint, resolve it to {}.

For Function, the return and parameter type is any, the parameter type is Array<any>.

To access function's parameter type, it might be good to reuse index access type. For example

type Callback = (a: Author) => void
type CallbackArg = Callback["arguments"] // [Author]
type CallbackReturn = Callback["returns"] // void, the access type is arguable

Related

#212
#1773

Edit: I think representing function via typed generic is a more complete solution. But it requires more typing features as prerequisites and rewriting all function type encoding.

Activity

  1. yortus commented on Nov 18, 2016

    @yortus
    Contributor

    See also #6606

  2. weswigham commented on Nov 19, 2016

    @weswigham
    Member

    In your specific example, wouldn't

    declare interface Vue {
      new <T extends {[k: string]: Function}>(config: {computed: { [K in keyof T]: () => T[K]}}): T
    }

    Be an equivalently correct type (without need for a special type function), provided T were inferred correctly?

  3. HerringtonDarkholme commented on Nov 19, 2016

    @HerringtonDarkholme
    ContributorAuthor

    Wesley Wigham (@weswigham) I believe T could not be inferred correctly for now by design: mapped type is not an inference site. #12114 (comment)

  4. weswigham commented on Nov 19, 2016

    @weswigham
    Member

    Herrington Darkholme (@HerringtonDarkholme) note the comment:

    You could speculate about the compiler manufacturing a structurally compatible type, but that is generally not something we do and there are lots of subtle ways it could go wrong and lead to confusing error messages

    Accumulating valuable use cases, such as this, is how things like this can change. 👍

  5. changed the title [-]Feature request: function return/parameter/this type [/-] [+]Feature request: Represent Function via generic type parameterized by its return/parameter/this type [/+] on Nov 20, 2016
  6. changed the title [-]Feature request: Represent Function via generic type parameterized by its return/parameter/this type [/-] [+]Feature request: represent Function via generic type parameterized by its return/parameter/this type [/+] on Nov 20, 2016
  7. IMRaziel commented on Nov 23, 2016

    @IMRaziel

    i have a use case that is similar.

    declare interface StoreProps<A> {
      actions: A
    }
    
    declare function Store<A> (props: StoreProps<A>): StoreImpl<A>;
    
    declare interface StoreImpl<A> {
      commit<CK extends keyof A, CT extends A[CK] & { (x: P): void }, P>(k: CK, r: P): void
    }
    
    let s = Store({
      actions: {
        act: function(x: number) {}
      }
    })
    
    s.commit("act", "")

    I'm trying to make type checker to derive second argument type using value of first argument here. And VSCode correctly shows that type of second generic parameter in call to s.commit("act", "") should be

    { (x: number): void } & { (x: string): void }

    on mouseover hint.
    untitled
    I was hoping that i could make compiler to derive type of A[CK] and give error if type is illegal. This type is obviously illegal (argument can't be string and number at the same time), so compilation should fail and VSCode should show it as error, but it currently doesn't.

    I think some kind of type deconstruction will be good here. So could write, for example,

    commit<CK extends keyof A, A[CK] is {(x: P): RT}>(k: CK, r: P): void

    , where is deconstructs A[CK] and gives access to function's argument type and return type, depending on literal type of first argument

    Edit: sorry, tested a bit more and i think i understand now, why i have no compilation error.

    let x: { (x: number): void } & { (x: string): void } = (p: string|number) => {}

    compiles as well, so looks like { (x: number): void } & { (x: string): void } doesn't mean x is of type string & number. Just that function should be able to be called with both string and number parameters, but type deconstruction would be still usefull, because i used this part CT extends A[CK] & { (x: P): void }, P just to tell compiler that A[CK] should be a function of certain type and i cannot do it without getting types of arguments from A[CK]

  8. KiaraGrouwstra commented on Nov 29, 2016

    @KiaraGrouwstra
    Contributor

    Tracking this as well, wanna type map over heterogeneous structures (tuples, objects).

  9. zpdDG4gta8XKpMCd commented on Dec 8, 2016

    @zpdDG4gta8XKpMCd

    @tycho01 looks at #1213

  10. Igorbek commented on Dec 13, 2016

    @Igorbek
    Contributor

    That's going to be a great step towards generalization of functions. I like it. However, it would not be enough. Next things to consider are:

  11. dead-claudia commented on Dec 27, 2016

    @dead-claudia

    I happen to need full on return type rewriting for my particular use case. To pull from #12381, I need a transformation from Original<T> to Wrapped<T> to properly type my API (I'm currently using type parameters and a whole lot of any, but that's incredibly ugly):

    interface Original<T> {
      [P in keyof T]: (...args) => R;
    }
    
    interface Wrapped<T> {
      [P in keyof T]: (...args) => Promise<R>;
    }
  12. paldepind commented on Mar 22, 2017

    @paldepind

    What is the status of this? It seems to me like adding the proposed ReturnType would be a very nice improvement for adding types to many real-world functions.

    Below is an example of a function that cannot be given a correct type without this feature (note that I use ReturnType in the definition of ApplyObject):

    type ApplyObject<O extends Record<string, Pair>> = {
      [K in keyof O]: ReturnType<O[K]>
    }
    
    // Takes an object of functions, calls each function and returns an object with the results
    function applyObject<O extends { [p: string]: () => any }>(object: O): ApplyObject<O> {
      const newObject: any = {};
      for (const key of Object.keys(object)) {
        newObject[key] = object[key](); // <- Note application here
      }
      return newObject;
    }
  13. dead-claudia commented on Mar 23, 2017

    @dead-claudia

    How about instead, create a new type-level operator (instead of a native generic) called returnof. To take Simon Friis Vindum (@paldepind)'s example:

    type ApplyObject<O extends Record<string, Pair>> = {
      [K in keyof O]: returnof O[K]
    }

    It fits better with the theme of using syntax instead of generics for primitive type-level operations.

  14. HerringtonDarkholme commented on Feb 13, 2018

    @HerringtonDarkholme
    ContributorAuthor

    I think this usage is already covered by conditional type. Closing.

  15. KiaraGrouwstra commented on Feb 14, 2018

    @KiaraGrouwstra
    Contributor

    Yeah. For reference, see this Fn type implementation based on a similar discussion at gcanti/typelevel-ts#8.
    Well, minus the this binding requested here. And no way to represent return types based on inputs.

  16. locked and limited conversation to collaborators on Jul 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs InvestigationThis issue needs a team member to investigate its status.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions