简体   繁体   中英

Optional property based on value of enum

I'm trying to get some typings to work for a react useReducer .

Basically I have an action that has an optional property ( data ) based on the value of another property - so if STATUS is VIEW or EDIT , the action must have the data property. I almost have something working, but there's one case (see below) where this fails.

I guess one way of doing this is by explicitly setting STATUS.NEW to not require the extra property ( { type: 'SET_STATUS'; status: STATUS.NEW } ), but I'm wondering if theres a better way. If in the future I added a bunch of different statuses then I'd have to specify each one to not require the data property.

Typescript Playground

enum STATUS {
    NEW = 'new',
    VIEW = 'view',
    EDIT = 'edit'
}

/*
    if status is 'view', or 'edit', action should also contain
    a field called 'data'
*/
type Action =
    | { type: 'SET_STATUS'; status: STATUS }
    | { type: 'SET_STATUS'; status: STATUS.VIEW | STATUS.EDIT; data: string; }


// example actions

// CORRECT - is valid action
const a1: Action = { type: 'SET_STATUS', status: STATUS.NEW }

// CORRECT - is a valid action
const a2: Action = { type: 'SET_STATUS', status: STATUS.VIEW, data: 'foo' }

// FAILS - should throw an error because `data` property should be required
const a3: Action = { type: 'SET_STATUS', status: STATUS.EDIT }

// CORRECT - should throw error because data is not required if status is new
const a4: Action = { type: 'SET_STATUS', status: STATUS.NEW, data: 'foo' }

And the second part of the question is how I'd incorporate this into a useCallback below. I would have thought that useCallback would be able to correctly infer the arguments into the appropriate action type.

/* 
    assume:
    const [state, dispatch] = useReducer(stateReducer, initialState)
*/

const setStatus = useCallback(
    (payload: Omit<Action, 'type'>) => dispatch({ type: 'SET_STATUS', ...payload }),
    [],
)

/* 
complains about:

Argument of type '{ status: STATUS.EDIT; data: string; }' is not assignable to parameter of type 'Pick<Action, "status">'.
  Object literal may only specify known properties, and 'data' does not exist in type 'Pick<Action, "status">'
*/
setStatus({ status: STATUS.EDIT, data: 'foo' })

You can define a union of statues that require data , then exclude them in action representing all the others:

enum STATUS {
    NEW = 'new',
    VIEW = 'view',
    EDIT = 'edit'
}

type WithDataStatuses = STATUS.VIEW | STATUS.EDIT;

type Action =
    | { type: 'SET_STATUS'; status: Exclude<STATUS, WithDataStatuses> }
    | {
        type: 'SET_STATUS';
        status: WithDataStatuses;
        data: string;
    }

// now CORRECT - data is required
const a3: Action = { type: 'SET_STATUS', status: STATUS.EDIT }

Answer for second part of question :-)

Assuming that you have defined Actions as suggested by @Aleksey L., useCallback can by typed as follows

// This is overloaded function which can take data or not depending of status
interface Callback {
  (payload: { status: Exclude<STATUS, WithDataStatuses> }): void;
  (payload: { status: WithDataStatuses; data: string; } ): void;
}

const [state, dispatch] = React.useReducer(stateReducer, {})
// Explicitly type useCallback with Callback interface
const setStatus = React.useCallback<Callback>(
  (payload) => dispatch({ type: 'SET_STATUS', ...payload }),
  [],
)
setStatus({ status: STATUS.EDIT, data: 'foo' })
setStatus({ status: STATUS.NEW })

The working demo

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM