I am not sure why the get
function is not strongly typed.
it is defined as get:(id:string) => IPromise<Test>;
but, I am assigning to it as
constructor(srv:Store<TestData>){
this.get = (id) => this.getImp(srv, id);
}
private getImp(srv:Store<TestData>, d:string) {
return srv.get("/test")
.then( (d) => { //d == IPromise<getImp>
var rep = d.reply; //rep == TestData
//var rep:string = "" gives error
return rep;
});
}
getImp
returns a prmise (result of get) with a typed parameter TestData
. But this should not be compatible with Test
, hence with get
. Why there is no error?
there is no error if I change the signature of getImp
as
private getImp(srv:Store<TestData>, d:string):IPromise<TestData> {
how getImp(srv:Store<TestData>, d:string):IPromise<TestData>
is assignable to get:(id:string) => IPromise<Test>
anything wrong with the definition of IPromise ?
thanks.
export interface IGetResult<T> {
reply: T
}
export class Store<T>{
get:(r:any) => IPromise<IGetResult<T>>;
}
export interface TestData {
Id: string
Name : string
}
export interface ITest extends TestData{
test():void
}
export class Test implements ITest {
Id:string;
Name:string;
test(){}
}
export class service{
get:(id:string) => IPromise<Test>;
constructor(srv:Store<TestData>){
this.get = (id) => this.getImp(srv, id);
}
private getImp(srv:Store<TestData>, d:string):IPromise<TestData> {
return srv.get("/test")
.then( (d) => { //d == IPromise<TestData>
var rep = d.reply; //rep == TestData
//var rep:string = "" gives error
return rep;
});
}
}
//========== angular.d.ts ===========
// from https://github.com/borisyankov/DefinitelyTyped/blob/master/angularjs/angular.d.ts
export interface IRequestConfig {
method: string;
url: string;
params?: any;
// XXX it has it's own structure... perhaps we should define it in the future
headers?: any;
cache?: any;
withCredentials?: boolean;
// These accept multiple types, so let's define them as any
data?: any;
transformRequest?: any;
transformResponse?: any;
timeout?: any; // number | promise
}
export interface IHttpPromiseCallback<T> {
(data: T, status: number, headers: (headerName: string) => string, config: IRequestConfig): void;
}
export interface IHttpPromiseCallbackArg<T> {
data?: T;
status?: number;
headers?: (headerName: string) => string;
config?: IRequestConfig;
}
export interface IHttpPromise<T> extends IPromise<T> {
success(callback: IHttpPromiseCallback<T>): IHttpPromise<T>;
error(callback: IHttpPromiseCallback<T>): IHttpPromise<T>;
then<TResult>(successCallback: (response: IHttpPromiseCallbackArg<T>) => TResult, errorCallback?: (response: IHttpPromiseCallbackArg<T>) => any): IPromise<TResult>;
then<TResult>(successCallback: (response: IHttpPromiseCallbackArg<T>) => IPromise<TResult>, errorCallback?: (response: IHttpPromiseCallbackArg<T>) => any): IPromise<TResult>;
}
export interface IPromise<T> {
then<TResult>(successCallback: (promiseValue: T) => IHttpPromise<TResult>, errorCallback?: (reason: any) => any, notifyCallback?: (state: any) => any): IPromise<TResult>;
then<TResult>(successCallback: (promiseValue: T) => IPromise<TResult>, errorCallback?: (reason: any) => any, notifyCallback?: (state: any) => any): IPromise<TResult>;
then<TResult>(successCallback: (promiseValue: T) => TResult, errorCallback?: (reason: any) => TResult, notifyCallback?: (state: any) => any): IPromise<TResult>;
catch<TResult>(onRejected: (reason: any) => IHttpPromise<TResult>): IPromise<TResult>;
catch<TResult>(onRejected: (reason: any) => IPromise<TResult>): IPromise<TResult>;
catch<TResult>(onRejected: (reason: any) => TResult): IPromise<TResult>;
finally<TResult>(finallyCallback: ()=>any):IPromise<TResult>;
}
Generics in TypeScript are of different design. In some cases they're bivariant WRT their type parameter, and you're expecting that they're covariant in this particular case. Promises are immutable so afaik they should indeed be covariant wrt the type they're holding.
To see that its so, change
export interface ITest extends TestData
to
export interface ITest
then remove the property Name:string;
from class Test
. If you do this, then typescript will finally complain that it "Cannot convert IPromise<TestData>
to IPromise<Test>
" because neither Test
can be derived from TestData
nor TestData
can be derived from Test
.
A more direct illustration of this bug:
var x: IPromise<Test> = <IPromise<TestData>><any>{}
As long as Test inherits from TestData or TestData inherits from Test, this remains valid.
Explanation of the terms covariant and bivariant
A generic type is covariant WRT its type parameter if its hierarchy varies together with the hierarchy of the type parameter. Example:
If
Array<T>
is covariant WRT the type T
andExtendedItem
is a subtype of Item
then
Array<ExtendedItem>
is accepted as a subtype of Array<Item>
.A generic type is bivariant WRT its type parameter if the subtyping goes both ways:
If
Array<T>
is bivariant WRT the type T
andExtendedItem
is a subtype of Item
then
Array<ExtendedItem>
is accepted as a subtype of Array<Item>
andArray<Item>
is accepted as a subtype of Array<ExtendedItem>
andFinal note: To be type-safe, a mutable Array should actually be invariant WRT to the argument. Many languages such as C# or Java make it covariant - but in this case its possible to push(item:OtherExtendedItem);
into an array of ExtendedItem
without it being caught as an error at compile time.
What does "sometimes bivariant" mean though? It means that typescript doesn't actually have any hard-coded variance rules. When it encounters a specialization of a generic type, it just applies that type parameter. After it does that, it performs a structural check of the resulting types - if they match, then everything is okay. Consider the following example:
class Box<T>{
public x: number;
constructor(t:T) {
this.x = 1;
}
}
interface AB { a: number; b: number; }
interface ABC { a: number; b: number; c: number; }
var ab:Box<AB> = new Box({a: 1, b: 2});
var y:Box<ABC> = ab;
The compiler considers this code valid. Whether the type has been specialized with T = AB or T = ABC doesn't change the structure of the resulting type, which is simply {x: number}
What happens if we have a method in Box that returns a Box? Lets add that method and see:
getThis():Box<T> { return this; }
Again, if we apply the specialization, we get an infinite, recursive structure:
{x: number, getThis: () => {x : number, getThis: () => ... }}
that ends up being the same regardless of the specialization.
But, if we add another method instead, one that retuns a value of type T:
getValue():T { return <T>{}; }
Then if we apply the specialization T = AB, it results with the following structure:
{x: number; y: () => { a: number; b number; } }
which is incompatible with the specialization T = ABC
{x: number; y: () => { a: number; b: number; c: number; } }
and the compiler will finally complain.
At this point, you may notice that IPromise<T>
isn't really quite like Box<T>
. Indeed, it does not have a method that returns a value of type T, but it has methods (like then
and catch
) that take callbacks which take values of type T. Lets add a method like that to Box
then<U>(f: (val: T) => Box<U>): Box<U>;
Now lets apply the type specialization T = AB
{x: number; then: (val: {a: number; b: number;}) => Box<{x: number; then: ...}}
and T = ABC
{x: number; then: (val: {a: number; b: number; c: number;}) => Box<{x: number; then: ...}}
These should not be compatible!
Which leads us to the true problem in TypeScript: arguments of function type ("callbacks") are bivariant WRT their input arguments, when they should be contravariant.
Here is a simple example that demonstrates the problem:
interface IA { bar(): void }
interface IB { bar(): void; foo(): void; }
function fn(passedFn: (a: IA) => void) {
var a:IA = {bar: function(){}};
passedFn(a);
}
// compiles but throws TypeError at runtime!
fn((b: IB) => b.foo());
In this example, a callback that takes an argument of type IA cannot be safely substituted with a callback that takes an argument of type IB (when IB extends IA). Like in the example, the callback taking the argument of type IB may try to call the method foo(), which means that passing an argument of type IA to that callback will result with a type error
The quickest fix for your issue would be to find a member to IPromise that returns a value of type T:
export interface IPromise<T> {
__bivariance_fix: T;
}
Of course this will make the interface incompatible with other promise types (or libraries), so the real fix here is for TypeScript to correct behavior of callbacks.