Reputation: 532
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
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