Reputation: 8948
In my jasmine-auto-spies project, I have the following lines -
export type Spy<T> = T & {
[k in keyof T]: AsyncSpyFunction;
}
export interface AsyncSpyFunction extends jasmine.Spy {
(...params: any[]): any;
and: AsyncSpyFunctionAnd
}
export interface AsyncSpyFunctionAnd extends jasmine.SpyAnd {
nextWith(value: any): void;
nextWithError(value: any): void;
resolveWith(value: any): void;
rejectWith(value: any): void;
}
What I'm trying to achieve is to turn all the functions of a class into spies.
So I can do:
let myServiceSpy: Spy<MyService> = createSpyFromClass(MyService);
And get an object which is both the class instance AND the spy.
So when I call one of its methods -
myServiceSpy.doSomething();
I can also set it up like this -
myServiceSpy.doSomething.and.returnValue('someVal');
In terms of types, I'd like to ignore getters and setters because they could easily be mocked by setting up a property.
And currently TypeScript complains if I try to set a property which has a setter function like this
MyService{
set url(value:string){
this._url = value;
}
}
let myServiceSpy: Spy<MyService> = createSpyFromClass(MyService);
And in my test I try to override it -
myServiceSpy.url = 'test';
I get this error -
Type '"test"' is not assignable to type 'string & AsyncSpyFunction'.
So I want to be able to ignore setters and getters in the type definition itself.
I want to be able to return an ObservableSpy when the original function returns an Observable.
Same goes for Promises and other async types.
Each of them has its own unique methods.
Currently I just use a catch all type which has all the possible implemented spy methods, but I prefer to be more specific.
So let's say I have the following class -
class MyService{
getUsers():Observable<User[]> {
return Observable.of(users);
}
getToken():Promise<Token> {
return Promise.resolve(token);
}
}
And in my test I'm setting up -
myServiceSpy.getUsers.and.nextWith(fakeUsers);
myServiceSpy.getToken.and.resolveWith(fakeToken);
I want the method nextWith
to appear only for methods which return Observables
And the resovleWith
to appear only for methods which return promises.
Is that possible to configure somehow?
Upvotes: 2
Views: 759
Reputation: 765
As the release of typescript 2.8, you can do it with mapped conditional types:
import { Observable } from "rxjs";
export type Spy<T> = { [k in keyof T]: AddSpyTypes<T[k]> };
export type AddSpyTypes<T> = T extends (...args: any[]) => any
? AddSpyByReturnTypes<T>
: T;
export interface PromiseSpy<T> {
resolveWith(value: T): void;
rejectWith(value: any): void;
}
export interface ObservableSpy<T> {
nextWith(value: T): void;
nextWithError(value: any): void;
}
export type AddSpyOnFunction<T extends (...args: any[]) => R, R> = T & {
and: jasmine.Spy;
};
export type AddSpyOnPromise<T extends Promise<any>> = T & {
and: PromiseSpy<Unpacked<T>>;
};
export type AddSpyOnObservable<T extends Observable<any>> = T & {
and: ObservableSpy<Unpacked<T>>;
};
// Wrap the return type of the given function type with the appropriate spy methods
export type AddSpyByReturnTypes<
TF extends (...args: any[]) => any
> = TF extends (...args: any[]) => infer TR // returnes a function
? TR extends (...args: any[]) => infer R2
? AddSpyOnFunction<TR, R2> // returnes a Promise
: TR extends Promise<any>
? AddSpyOnPromise<TR> // returnes an Observable
: TR extends Observable<any> ? AddSpyOnObservable<TR> : TF
: never;
//github.com/Microsoft/TypeScript/issues/21705#issue-294964744
export type Unpacked<T> = T extends (infer U)[]
? U
: T extends (...args: any[]) => infer U
? U
: T extends Promise<infer U> ? U : T extends Observable<infer U> ? U : T;
old and obsolete answer:
* This is not suppose to be a working solution, but a step toward one *
My immediate thinking would to supply more data to TS. This is far from perfect, i hope its possible to get more with TS
Another issue is that, we can't(?) really infere the types for resolveWith/rejectWith.
If you will supply the types of the props on P and PO interfaces, and not only the props names, we can use them.
// p is interface with the the promise props, PO is an interface with the observer props
export type Spy<T, P, PO> = T & {
[k in keyof P]: AsyncSpyFunctionAnd<P[k]>;
} & {
[k in keyof PO]: AsyncSpyFunctionAndObserver<PO[k]>;
}
export interface AsyncSpyFunction<T> extends jasmine.Spy {
(...params: any[]): any;
and: AsyncSpyFunctionAnd<T>;
}
export interface AsyncSpyFunctionAnd<T> extends jasmine.SpyAnd {
resolveWith(value: T): void;
rejectWith(value: any): void;
}
export interface AsyncSpyFunctionAndObserver<T> extends jasmine.SpyAnd {
nextWith(value: T): void;
nextWithError(value: any): void;
}
And on typescript playground: link
Upvotes: 3