LaurentEsc
LaurentEsc

Reputation: 31

TypeScript convert object from type with optional property to type without optional property

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

Answers (1)

HTN
HTN

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

Related Questions