Home > Back-end >  Use generics to define return value for function with discriminated union as input
Use generics to define return value for function with discriminated union as input

Time:05-17

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.

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!

CodePudding user response:

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

  • Related