Fabian Buentello
Fabian Buentello

Reputation: 532

Typescript generic interfaces match on property value

I am still learning TypeScript and all the features it has. One of which is constraining generics. I apologize if this is a commonly asked question, if you have any resources(beyond the docs) that can help me get better at this, please link as comment.

What I'm trying to do is, have the type properties match between my DeliveryObject and all the objects inside of deliveryItems.

Here's an example of code that compiles, but just isn't the end solution that I'm looking for.

type DeliveryMethod = 'notification' | 'text' | 'email'

type DeliveryActions = INotificationAction | ITextMessageAction | IEmailAction

interface IDelivery {
  type: DeliveryMethod
}

interface INotificationAction extends IDelivery {
  type: 'notification'
  deviceId: string
  message: string
}

interface ITextMessageAction extends IDelivery {
  type: 'text'
  message: string
}

interface IEmailAction extends IDelivery {
  type: 'email'
  to: string
  subject: string
  body: string
}

// I know I need to do something additional here...
interface IDeliveryObject<T extends DeliveryMethod, U extends DeliveryActions> {
  type: T
  deliveryItems: Array<U>
}

function sendDelivery<K extends DeliveryMethod, Z extends DeliveryActions>(state: IDeliveryObject<K, Z>) {
  console.log(state)
}

sendDelivery({
  type: 'notification', // <--- needs to match or error out
  deliveryItems: [
    {
      type: 'email',  // <--- needs to match or error out
      to: '[email protected]',
      subject: '1235-67890',
      body: 'Here is a notification'
    }
  ]
})

Upvotes: 2

Views: 2193

Answers (1)

CRice
CRice

Reputation: 32166

I would approach this by using a "type lookup map" to tie together the delivery method and it's associated action object. So I would add another type like this:

type DeliveryActionTypes = {
    "notification": INotificationAction;
    "text": ITextMessageAction;
    "email": IEmailAction;
}

That type just maps the correct method name to it's action object type. Then you can replace the declarations for DeliveryMethod and DeliveryActions with:

type DeliveryMethod = keyof DeliveryActionTypes;
type DeliveryActions = DeliveryActionTypes[keyof DeliveryActionTypes];

That will allow you to easily lookup the correct action if you know the name of the method. You can use that in your IDeliveryObject to make sure that the two types correspond:

interface IDeliveryObject<T extends DeliveryMethod> {
    type: T;

    // This is the type lookup, note the `DeliveryActionTypes[T]` part.
    deliveryItems: Array<DeliveryActionTypes[T]>;
}

Now you can simplify the signature for the sendDelivery function, since all it needs now is the method name:

function sendDelivery<K extends DeliveryMethod>(state: IDeliveryObject<K>) {
  console.log(state)
}

With all that, you'll get an error if the types don't match:

sendDelivery({
  type: 'notification',
  deliveryItems: [
    {
      type: 'email', 
      to: '[email protected]', // <-- error on this line, see below
      subject: '1235-67890',
      body: 'Here is a notification'
    }
  ]
})
Type '{ type: "email"; to: string; subject: string; body: string; }'
is not assignable to type 'INotificationAction'.

As you can see, Typescript correctly concludes that the items in the array should be of type INotificationAction, and produces an error when they are not.

Upvotes: 4

Related Questions