Wednesday, April 18, 2018

Completely solve a problem with Partial


"Whenever I write code in Java I feel like I'm filling out endless forms in triplicate." -- http://funcall.blogspot.com/2010/04/whenever-i-write-code-in-java.html


In order to appease the compiler from complaining this:





I had introduced another interface just to make the properties optional:

type ActionsUsed =
    IUserAction
    | IUserCancelledAction
    | ICompanyAction
    | ICounterAction
    | IErrorModuleAction
    | ILoggedUserAction
    ;


export interface IActionsForTest
{
    counter?: {
        increment: () => void;
        decrement: () => void;
    };

    goToUser?: (userId: number) => void;
    cancelUserLoading?: () => void;

    goToCompany?: (code: string) => void;

    tellModuleError?: (error: IErrorHappened) => void;

    saveUser?: (loggedUser: ILoggedUser) => void;

    goToLogin?: () => void;
}




export interface IActionsRequired
{
    counter: {
        increment: () => void;
        decrement: () => void;
    };

    goToUser: (userId: number) => void;
    cancelUserLoading: () => void;

    goToCompany: (code: string) => void;

    tellModuleError: (error: IErrorHappened) => void;

    saveUser: (loggedUser: ILoggedUser) => void;

    goToLogin: () => void;
}


export interface IDispatches
{
    actions: IActionsRequired;
}


// Redux's built-in Dispatch can't check if a wrong value is passed to dispatch's type parameter.
// So we make our own Dispatch type.
type Dispatch<A> = (action: A) => A;

export const mapDispatchToProps = (dispatch: Dispatch<ActionsUsed>): IDispatches =>
    ({
        actions: {
            counter: {
                increment: () => dispatch({type: CounterKind.INCREMENT}),
                decrement: () => dispatch({type: CounterKind.DECREMENT})
            },

            goToUser         : (userId: number) => dispatch({type: UserActionType.USER, payload: {userId}}),
            cancelUserLoading: () => dispatch({type: UserActionType.USER_CANCELLED}),

            goToCompany: (code: string) => dispatch({type: CompanyActionType.COMPANY, payload: {code}}),

            tellModuleError: (errorHappened: IErrorHappened) => dispatch({
                type: 'ERROR_MODULE', payload: errorHappened
            }),

            saveUser: (loggedUser: ILoggedUser) =>
                dispatch({type: LoggedUserActionType.SAVE_USER_INFO, payload: loggedUser}),

            goToLogin: () => dispatch({type: Routes.LOGIN} as any)
        }
    });





So no more problems on testing:




But it looks ridiculous to write another interface just for testing.

Another way to solve the problem is to make all properties of the IActionsRequired optional by suffixing the question mark on all properties:

export interface IActionsRequired
{
    counter?: {
        increment: () => void;
        decrement: () => void;
    };

    goToUser?: (userId: number) => void;
    cancelUserLoading: () => void;

    goToCompany?: (code: string) => void;

    tellModuleError?: (error: IErrorHappened) => void;

    saveUser?: (loggedUser: ILoggedUser) => void;

    goToLogin?: () => void;
}

Testing won't require all the properties of IActionsRequired:




It would introduce compilation error when the optional property is used though:







It can be solved by using non-null assertion operator, however, it would introduce exclamation noises in the code of the consumer of the IActionsRequired, e.g.,

this.props.actions.counter!.decrement

this.props.actions.saveUser!(loggedUser)


Then I found a generic interface that makes a property of an interface optional, it would solve the problem of requiring all the properties of interface for the test object. That generic interface is called Partial. Let's restore IActionsRequired's properties as required by removing all the question marks:



And then on test, use Partial on IActionsRequired:




Partial is part of TypeScript, no need to npm install / yarn add anything, nor import Partial. Here is its definition:




Happy Coding!

No comments:

Post a Comment