Reputation: 31
I'm new to TypeScript and I am facing a pretty trivial problem. I am using a third party email library that I wrap in a service function that I control. The third party function as well as my service function accept an object with common email configuration properties (to, from, subject, body ...), but the third party function requires all of these while my service lets the from property be optional. See below.
// Third Party Code
type ThirdPartyEmailData = {
from: string;
to: string;
subject: string;
body: string;
}
const sendEmailWithThirdPartyService = (emailData: ThirdPartyEmailData) => {
console.log('Sending email from:', emailData);
}
-----------------------------------------------------------------------------
// My Code
type MyEmailData = {
from?: string;
to: string;
subject: string;
body: string;
}
const sendEmail = (emailData: MyEmailData) => {
emailData = { from: '[email protected]', ...emailData };
sendEmailWithThirdPartyService(emailData); // This does not compile
/**
* Argument of type 'MyEmailData' is not assignable to parameter of type 'ThirdPartyEmailData'.
* Property 'from' is optional in type 'MyEmailData' but required in type 'ThirdPartyEmailData'.
*/
}
sendEmail({
to: '[email protected]',
subject: 'I <3 TypeScript',
body: 'But TypeScript hates me'
});
As expected, the above does not compile because the type 'MyEmailData' is not assignable to parameter of type 'ThirdPartyEmailData'. I have been looking for a clean way to convert/change/cast the type of emailData before I pass it to the third party function, but I can'r really find a good way. Below are a few things I came up with, but it does not feel good.
Solution 1: just cast to the third party type
// My Code
const sendEmail = (emailData: MyEmailData) => {
// If you remove the line below the program still compiles but breaks at runtime
emailData = { from: '[email protected]', ...emailData };
sendEmailWithThirdPartyService(<ThirdPartyEmailData>emailData); // This compiles
}
Solution 2: use a type guard to ensure the from property is present
// My Code
const sendEmail = (emailData: MyEmailData) => {
emailData = { from: '[email protected]', ...emailData };
// By using the type guard, we ensure that the type of emailData overlaps ThirdPartyEmailData
if (! hasFromProperty(emailData)) throw new Error('From property is missing');
sendEmailWithThirdPartyService(emailData); // This compiles
}
-----------------------------------------------------------------------------
// Utilities
/** Utility to make certain keys of a type required */
type RequiredKeys<T, K extends keyof T> = Exclude<T, K> & Required<Pick<T, K>>
/** Typeguard for property 'from' in MyEmailData */
const hasFromProperty = (data: MyEmailData): data is RequiredKeys<MyEmailData, 'from'> => {
return 'from' in data;
}
This looks like a very common problem, yet I have not been able to find a satisfying solution. What can you recommend ?
Upvotes: 3
Views: 1499
Reputation: 3604
You should avoid mutating a parameter. In your case, the solution is simple:
const sendEmail = (emailData: MyEmailData) => {
const thirdPartyEmailData: ThirdPartyEmailData = { from: '[email protected]', ...emailData };
sendEmailWithThirdPartyService(thirdPartyEmailData);
}
It works even without declaring the type of thirdPartyEmailData const thirdPartyEmailData = { from: '[email protected]', ...emailData };
as it is inferred.
Upvotes: 1