Description
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.