0

I've got two subtypes in our application:

export interface QueuedAction extends Action {
  meta: {
    queueName: string;
    overwrite: boolean;
  }
}

export interface BroadcastAction extends Action {
  meta: {
    channelName: string;
  }
}

And based on typescript interface require one of two properties to exist I union them like so:

export type MetaAction = QueuedAction | BroadcastAction;

That's great and all, but I sometimes need to act on a MetaAction where one half of the inclusive union is guaranteed.

This has issues:

const wrapBroadcast = (action: AnyAction, channelName = 'DEFAULT_CHANNEL'): MetaAction => ({
  ...action,
  meta: { ...action.meta, channelName },
});

const unwrapBroadcast = (action: MetaAction) => {
  if (!action.meta) return action;

  const { channelName, ...rest } = action.meta; // channelName does not exist on type '{ queueName: string, overwrite?: boolean} | { channelName: string }'
  const unwrappedAction = { ...action, meta: rest };
  return unwrappedAction;
}

And if I switch it from MetaAction to BroadcastAction | {BroadcastAction & MetaAction} (to say, it's always going to have channelName but the rest of meta is flexible). I get this error when I go to use it:

Argument of type 'MetaAction' is not assignable to parameter of type 'BroadcastAction & MetaAction'

Until I swap the return type of wrapAction to match.

Is there a more generic way to declare BroadcastAction | {BroadcastAction & MetaAction} given in the future we may end up with more stuff in 'meta' and we'd need this pattern elsewhere?

AncientSwordRage
  • 7,086
  • 19
  • 90
  • 173

1 Answers1

1

Make MetaAction generic

Any easy way to make this pattern more generic is to make it ... generic:

export type MetaAction<T extends { meta: any } | {} = {}> = T & (QueuedAction | BroadcastAction); 

That is, MetaAction takes an option argument T, that must extend anything with { meta: any }, or {}.

And for even more type safety, make your specific MetaActions extend a Base type:

// no meta specified, any action from redux allows for any properties
interface BaseMetaAction extends AnyAction {};

export interface QueuedAction extends BaseMetaAction {
  // as before
}

export interface BroadcastAction extends BaseMetaAction {
  // as before
}

export type MetaAction<T extends BaseMetaAction = AnyAction> = T & (QueuedAction | BroadcastAction); 

Now you can have MetaAction with or without a required subtype, so long as that subtype extends the BaseMetaAction.

Here's a playground of this setup.


Here's how you would and would not use it:

// does work
const myQueuedAction: MetaAction<QueuedAction> = {
  type: 'foo':
  meta: {
    queueName: 'THE_FOO_QUEUE',
  }
};

const myQueuedSometimesBroadcastAction: MetaAction<QueuedAction> = {
  type: 'foo':
  meta: {
    queueName: 'THE_FOO_QUEUE',
    channelName: 'THE_FOO_BROADCAST',
  }
};

const myBroadcastSometimesQueuedAction: MetaAction<BroadcastAction> = {
  type: 'foo':
  meta: {
    queueName: 'THE_FOO_QUEUE',
    channelName: 'THE_FOO_BROADCAST',
  }
};

// does not work
const myQueuedSometimesBroadcastAction: MetaAction<QueuedAction> = {
  type: 'foo':
  meta: {
    channelName: 'THE_FOO_BROADCAST',
  }
};

const myBroadcastSometimesQueuedAction: MetaAction<BroadcastAction> = {
  type: 'foo':
  meta: {
    queueName: 'THE_FOO_QUEUE',
  }
};

// constraint not satisfied
type NotASubtype = { meta: {meta: "I'm so meta even this acronym" } }
const myWildMetaAction: MetaAction<NotASubtype> = {
  type: 'foo':
  meta: {
    queueName: 'THE_FOO_QUEUE',
  }
};
AncientSwordRage
  • 7,086
  • 19
  • 90
  • 173