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

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

Closed
HerringtonDarkholme opened this issue Nov 18, 2016 · 13 comments
Labels
Needs Investigation This issue needs a team member to investigate its status.

Comments

@HerringtonDarkholme
Copy link
Contributor

HerringtonDarkholme commented Nov 18, 2016

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.

@yortus
Copy link
Contributor

yortus commented Nov 18, 2016

See also #6606

@weswigham
Copy link
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?

@HerringtonDarkholme
Copy link
Contributor Author

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

@weswigham
Copy link
Member

weswigham commented Nov 19, 2016

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

@HerringtonDarkholme HerringtonDarkholme changed the title Feature request: function return/parameter/this type Feature request: Represent Function via generic type parameterized by its return/parameter/this type Nov 20, 2016
@HerringtonDarkholme HerringtonDarkholme 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 Nov 20, 2016
@IMRaziel
Copy link

IMRaziel commented Nov 23, 2016

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]

@KiaraGrouwstra
Copy link
Contributor

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

@zpdDG4gta8XKpMCd
Copy link

@tycho01 looks at #1213

@Igorbek
Copy link
Contributor

Igorbek commented Dec 13, 2016

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:

@dead-claudia
Copy link

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>;
}

@paldepind
Copy link

paldepind commented Mar 22, 2017

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;
}

@dead-claudia
Copy link

How about instead, create a new type-level operator (instead of a native generic) called returnof. To take @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.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label May 24, 2017
@HerringtonDarkholme
Copy link
Contributor Author

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

@KiaraGrouwstra
Copy link
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.

@microsoft microsoft locked and limited conversation to collaborators Jul 3, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

No branches or pull requests

10 participants