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] contextually type partially annotated function #11475

Closed
HerringtonDarkholme opened this issue Oct 10, 2016 · 3 comments
Closed
Labels
Duplicate An existing issue was already created

Comments

@HerringtonDarkholme
Copy link
Contributor

HerringtonDarkholme commented Oct 10, 2016

Background:

JavaScript has very dynamic nature. Often library can only give imprecise typed API and let users annotate their code to give more specific types. However, given TypeScript's contextual typing spec, library API can lose typing information and requires user to annotate more code than necessary.

Example1. JQuery's XHR:

jQuery's XHR is deferred like object that has a then method. then method will be called with data fetched from server. This is the typing from @types/jquery

then<R>(doneCallback: (data: any, textStatus: string, jqXHR: JQueryXHR) => R, failCallback?: (jqXHR: JQueryXHR, textStatus: string, errorThrown: any) => void): JQueryPromise<R>;

Users will want to annotate data because they know what will be returned, but this will break contextual typing :

$.get('server/url').then((data: MyData, textStatus, jqXHR) => {
  // data has type MyData, but jqXHR is inferred as any
})

Two alternatives, but both require more annotation

// annotate all
$.get('server/url').then((data: MyData, textStatus: string, jqXHR: JQueryXHR) => {})
// or annotate none, cast later
$.get('server/url').then((data, textStatus , jqXHR) => {
  let myData = data as MyData
})

Similar examples can also be found in angularjs,

Example 2. this injection in jQuery and Vue

Some JavaScript libraries heavily depends on injecting this to user provided function. A classic example is jquery's event listening code.

And with #6739, TypeScript has great support to inject a typed this to function via contextual typing. However, when function's parameter is annotated, contextual typing is skipped.

A jQuery example.

$('div').input(function(e: JQueryClickEvent) {
  var name = this.getAttribute('id');  // this is typed any
});

vue heavily depends on this injection for passing context to method definition. Supporting partial annotated function can help reducing explict this type annotation via a typed API. See #10461 (comment) for example.

VueTyped
  .data({a: 1})
  .method(function (this, n: number) { // unannotated this can be typed
     this. a += n
  })

Example3. context injection in koa-route and redux

Some libraries will provide users functionality to specify their own handlers for different actions. For example, koa-route users can specify request handlers by app.get('path', handler). redux users can specify reducers (a handler to update state).

These handler declaration will all have a leading parameter such as context or state for handlers to access application state. And rest parameters will stand for arguments to trigger handlers.

// in koa
koaApp.use(koaRoute.get('pet/:id', (ctx, id: number) => {
    // ctx is any here
   let request = ctx.request
}))

// in redux

const reducer: Reducer = function (state, a: Action) {
   // state is any here
}

In some library, context type is computed by compiler, which isn't possible for user to manually annotate.

Context typing can be done by currying, but this is bad for API design and incurs unnecessary runtime overhead.

Proposal

We first to define what does partially annotated function mean.

A partially annotated function is a function expression that has no parameter or has at least one parameter missing type annotation.

No parameter is for compatibility like below.

interface A { contextualTypeThis(this: {a: number}): void }
var a: A = { contextualTypeThis: function() { this.a } }

To enable contextual typing on partially annotated function requires several modification to spec.

In #4.10

When a function expression with no type parameters and no parameter type annotations is contextually typed.

is changed to

When a function expression with no type parameters and is partially annotated is contextually typed.

And in #4.23

In a typed function call, argument expressions are contextually typed by their corresponding parameter types.

is changed to

In a typed function call, argument expressions are contextually typed by their corresponding parameter types, if not manually annotated. Otherwise the type of an argument expression is used to inferentially type its parameter type, if the parameter type can contain type parameters.

Note #4.15.2 is not changed. Namely these two rules:

(Type parameter inference) Proceeding from left to right, each argument expression e is inferentially typed by its corresponding parameter type P, possibly causing some inferred type arguments to become fixed....

And

When a function expression is inferentially typed (section 4.10) and a type assigned to a parameter in that expression references type parameters for which inferences are being made, the corresponding inferred type arguments to become fixed and no further candidate inferences are made for them.

In short, preceding arguments are inferred as if following arguments are not annotated.
Thus, this code will break.

declare function bound<T, R extends T>(f: (r: R, t: T) => void): R
let result = bound((r, t: { break: number }) => {})  // typed as {}
result.break // error

Because R is inferred as {} in t, and T is inferred as {} accordingly. And because typeof t is assignable to {}, so result is typed as {}, ignoring users' annotation.

Compatiblity

This will introduce breaking changes. But I have no idea how will it impact real world code

A parameter used to be inferred as any will become inferred to some type. This is a breaking change.

The above example is also a breaking change, the return type is currently inferred as any.

Overloading resolution probably is not influenced because contextual typing will succeed for a unannotated argument.

Implementaion

I have a experimental branch for partial annotation, and some new tests (still needs a lot more). It seems modification to existing code is not massive.

@HerringtonDarkholme
Copy link
Contributor Author

@ahejlsberg @RyanCavanaugh @mhegazy What's your opinion?

@RyanCavanaugh
Copy link
Member

I believe this is strictly a duplicate of #4241 ?

@HerringtonDarkholme
Copy link
Contributor Author

I'm so sorry I didn't manage to find that. Move to #4241

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Oct 11, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

2 participants