Shai Reznik - HiRez.io
Shai Reznik - HiRez.io

Reputation: 8948

How can I return a property type based on a condition?

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');

BUT... I'm missing 2 things:

1. The ability to ignore getter and setter functions

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.

2. Return a different spy types based on the original return types

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

Answers (1)

Bnaya
Bnaya

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

Related Questions