Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Use generics to define return value for function with discriminated union as input

I want to create a function that receives an object with updatedAt and/or createdAt properties (as a Date) and returns the same object but with the single or both values serialized as a string.

First of all, how do I define this function’s return type?

Second, I have a feeling that this is better done with generics, but I haven’t found the correct way to do it that way.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

This is what I currently have:

type HasBothValues = {
  [key: string]: any;
  createdAt: Date;
  updatedAt: Date;
};

type HasCreatedAt = {
  [key: string]: any;
  createdAt: Date;
};

type HasUpdatedAt = {
  [key: string]: any;
  updatedAt: Date;
};

type AllInputVariants = HasBothValues | HasCreatedAt | HasUpdatedAt;

const serializeDates = (obj: AllInputVariants): any => { //  <-- Don't like the any return type!
  const retObj = { ...obj };
  if (obj.createdAt) {
    retObj.createdAt = obj.createdAt.toISOString();
  }

  if (obj.updatedAt) {
    retObj.updatedAt = obj.updatedAt.toISOString();
  }

  return obj;
};

export { serializeDates };

I appreciate any tips!

>Solution :

Function overloading seems to be a good solution here.

First we create a type called Overwrite to change the types of some properties.

type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U extends infer O ? {
  [K in keyof O]: O[K]
} : never

Now we can add all possible function overloads:

function serializeDates<T extends HasBothValues>(obj: T): Overwrite<T, {updatedAt: string, createdAt: string}>
function serializeDates<T extends HasUpdatedAt>(obj: T): Overwrite<T, {updatedAt: string}>
function serializeDates<T extends HasCreatedAt>(obj: T): Overwrite<T, {createdAt: string}>
function serializeDates<T extends AllInputVariants>(obj: T) { 
  const retObj: any = { ...obj };
  if (obj.createdAt) {
    retObj.createdAt = obj.createdAt.toISOString();
  }

  if (obj.updatedAt) {
    retObj.updatedAt = obj.updatedAt.toISOString();
  }

  return retObj;
};

Playground


Let’s see if it works:

const t1 = serializeDates({
  createdAt: new Date(),
})
// const t1: {
//     createdAt: string;
// }

const t2 = serializeDates({
  updatedAt: new Date()
})
// const t2: {
//     updatedAt: string;
// }

const t3 = serializeDates({
  createdAt: new Date(),
  updatedAt: new Date()
})
// const t3: {
//     updatedAt: string;
//     createdAt: string;
// }

const t4 = serializeDates({
  createdAt: new Date(),
  updatedAt: new Date(),
  a: 123,
  b: "123"
})
// const t4: {
//     a: number;
//     b: string;
//     updatedAt: string;
//     createdAt: string;
// }

On second thought, we can do better. Let’s modify Overwrite to only override properties of T which actually exist on T.

type Overwrite<T, U> = (Pick<T, Exclude<keyof T, keyof U>> & Pick<U, Extract<keyof U, keyof T>>) extends infer O ? {
  [K in keyof O]: O[K]
} : never

Now we don’t need function overloading anymore:

function serializeDates<
  T extends { updatedAt?: Date, createdAt?: Date }
>(obj: T): Overwrite<T, {updatedAt: string, createdAt: string}> { 
  const retObj: any = { ...obj };
  if (obj.createdAt) {
    retObj.createdAt = obj.createdAt.toISOString();
  }

  if (obj.updatedAt) {
    retObj.updatedAt = obj.updatedAt.toISOString();
  }

  return retObj as any;
};

Playground

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading