TypeScript Separate Union Type Fields

My aim is to create a function which has two parameters. First one is union value of type field in Action and second parameter should force developer to use specific object in Action.

For example, assume that we have this function function dispatch(actionType, payload){}

dispatch('SIGN_OUT', { type: 'SIGN_OUT', userName: '' });

or

dispatch('SIGN_IN_FAILURE', { type: 'SIGN_IN_FAILURE', error: '' });

type Action =
    | { type: 'INIT' }
    | { type: 'SYNC' }
    | { type: 'SIGN_UP', userName: string, password: string, attributeList: Array<any> }
    | { type: 'SIGN_IN', userName: string, pasword: string }
    | { type: 'SIGN_IN_SUCCESS', accessToken: string }
    | { type: 'SIGN_IN_FAILURE', error: string }
    | { type: 'SIGN_OUT', userName: string }
    | { type: 'FORGOT_PASSWORD', userName: string }
    | { type: 'FORGOT_PASSWORD_SUCCESS', verificationCode: string }
    | { type: 'CHANGE_PASSWORD', userName: string, oldPassword: string, newPassword: string }
    | { type: 'CONFIRM_REGISTRATION', userName: string, confirmationCode: string }
    | { type: 'RESEND_CONFIRMATION_CODE', userName: string }
    | { type: 'DELETE_USER', userName: string }
    | { type: 'GET_SESSION', userName: string }
    | { type: 'GET_SESSION_SUCCESS', session: string };

type TodoAction =
    | { type: 'INIT' }
    | { type: 'ADD_TODO', text: string }
    | { type: 'REMOVE_TODO', id: string }
    | { type: 'SET_COMPLETED', id: string };

Wrong usage

Notice that payload parameter in line 32 is Action type, so I can create any object in Action type.

type DispatchType = (actionType: Action['type'], payload: Extract<Action, { type: Action['type'] }>) => void;

const dispatch: DispatchType = (actionType, payload) => { throw new Error('Not implemented') };

dispatch('CHANGE_PASSWORD', { type: 'DELETE_USER', userName: '' });

Still Wrong usage

Notice that when I write comma character in line 39, intellisense popup show me that I can use { type: 'CHANGE_PASSWORD', ....}, this is expected thing.

But I could use { type: 'DELETE_USER', ....} , so it is not expected behaviour, VSCode should error normally.

type DispatchType = <K extends Action['type']>(actionType: K, payload: Extract<Action, { type: K }>) => void;

const dispatch: DispatchType = (actionType, payload) => { throw new Error('Not implemented') };

dispatch('CHANGE_PASSWORD', { type: 'DELETE_USER', userName: '' });

Good usage

type DispatchType = <K extends Action['type'], J extends Extract<Action, { type: K }>>(actionType: K, payload: J) => void;

const dispatch: DispatchType = (actionType, payload) => { };

dispatch('SIGN_IN', { type: 'SIGN_IN', userName: '', pasword: '' });

dispatch('CONFIRM_REGISTRATION', { type: 'CONFIRM_REGISTRATION', userName: '', confirmationCode: '' });

Good usage

Notice that I just add a new generic type T extends Action and it is used in K extends T['type'] so instead of using K extends Action['type'], K extends T['type'] is used.

type DispatchType = <T extends Action, K extends T['type']>(actionType: K, payload: Extract<T, { type: K }>) => void;

const dispatch: DispatchType = (actionType, payload) => { throw new Error('Not implemented') };

dispatch('DELETE_USER', { type: 'DELETE_USER', userName: '' });

dispatch('CONFIRM_REGISTRATION', { type: 'CONFIRM_REGISTRATION', userName: '', confirmationCode: '' });

Better usage

type DispatchType = <T extends Action, K extends T['type'], J extends Extract<T, { type: K }>>(actionType: K, payload: J) => void;

const dispatch: DispatchType = (actionType, payload) => { };

dispatch('SIGN_IN', { type: 'SIGN_IN', userName: '', pasword: '' });

dispatch('GET_SESSION_SUCCESS', { type: 'GET_SESSION_SUCCESS', session: '' });

Ideal usage

Notice that I moved T extends { type: any }> in DispactType<T extends { type: any }> so that I could use dispatch for Action type and TodoAction type.

type DispatchType<T extends { type: any }> = <K extends T['type'], J extends Extract<T, { type: K }>>(actionType: K, payload: J) => void;

const dispatch: DispatchType<Action> = (actionType, payload) => { };

dispatch('SIGN_IN', { type: 'SIGN_IN', userName: '', pasword: '' });
const dispatch_: DispatchType<TodoAction> = (actionType, payload) => { };

dispatch_('ADD_TODO', { type: 'ADD_TODO', text: '' });

dispatch_('SET_COMPLETED', { type: 'SET_COMPLETED', id: '' });

Almost Best usage

Notice that payload doesn't require type field anymore.

type DispatchType<T extends { type: any }> = <K extends T['type'], J extends Omit<Extract<T, { type: K }>, 'type'>>(actionType: K, payload: J) => void;

const dispatch: DispatchType<Action> = (actionType, payload) => { throw new Error('Not implemented') };

dispatch('INIT', {});

dispatch('SIGN_IN', { userName: '', pasword: '' });

dispatch('CHANGE_PASSWORD', { userName: '', oldPassword: '', newPassword: '' });

Maybe better than best usage

type DispatchType<T extends { type: any }> = <K extends T['type'], J extends Omit<Extract<T, { type: K }>, 'type'>>(actionType: K, payload: J) => void;

const dispatch: DispatchType<Action> = (actionType, payload) => { throw new Error('Not implemented') };

dispatch('INIT', {});

dispatch('SIGN_IN', { userName: '', pasword: '' });

dispatch('CHANGE_PASSWORD', { userName: '', oldPassword: '', newPassword: '' });

Leave a Reply