1

I'm attempting to type an async arrow function that can take a single image object or an array of image objects.

TypeScript overloading is new to me and I may be doing everything completely wrong.

Here the attempt.

type ImageType = {
  uri: string;
  type?: string;
  height?: number;
  width?: number;
};

type ConvertImagesToJpegParams = {
  (param: ImageType): Promise<ImageType>;
  (param: ImageType[]): Promise<ImageType[]>;
};

const convertImagesToJpegAsync: ConvertImagesToJpegParams = async (images) => {
  const isImagesAnObject = typeof images === 'object';
  const imagesToConvert = isImagesAnObject ? [images] : images;

  let convertedImages = [];
  const convertImageToJpegPromises = imagesToConvert.map(async (image) => {
     // do stuff that converts the image.
  });
  await Promise.all(convertImageToJpegPromises);

  return isImagesAnObject ? convertedImages[0] : convertedImages;
};


  • How should I type async (images)?
    If I use images: ImageType | ImageType[] the map complains.

    Property 'map' does not exist on type 'ImageType | ImageType[]'. Property 'map' does not exist on type 'ImageType'.ts(2339)

  • Once images is properly typed is there a better way to test for isImagesAnObject? I thought something like images isinstanceof ImageType but that was a fail.

GollyJer
  • 23,857
  • 16
  • 106
  • 174
  • classical [overloading is not supported](https://stackoverflow.com/questions/13212625/typescript-function-overloading) just try to avoid it all together since you will have two functions doing different things with the same name. Like synonyms in languages... – fubar Jan 09 '20 at 00:31
  • That seems very dangerous; it's normal to have the input argument be of multiple types, but the return type should always be one type. Why not always return an array? – Kousha Jan 09 '20 at 00:46
  • My thought process was to reduce the work on the consumer side. With this you can send one in and get one back or send many in and get many back. May or may not be the best approach ‍♀️ but it was a good leaning experience. – GollyJer Jan 09 '20 at 00:52
  • 1
    @GollyJer you can use type-guard to achieve what you want. Check out my answer – Kousha Jan 09 '20 at 01:10

2 Answers2

1

Unfortunately, I don't think there's any way around specifically denoting the items type as being either ImageType or ImageType[]. Then check if the argument is an array using Array.isArray, and either return a mapped Promise.all on the asynchronous operation on the array, or just the asynchronous operation on the item:

const convertImagesToJpegAsync: ConvertImagesToJpegParams = async (images: ImageType | ImageType[]) => {
    return Array.isArray(images)
        ? Promise.all(images.map(Promise.resolve)) // insert your actual async operation here
        : Promise.resolve(images); // insert your actual async operation here
};

Typescript can infer the type of the parameters and their associated return types just fine without needing the ConvertImagesToJpegParams though, so feel free to leave that out (and remove that type definition entirely):

const convertImagesToJpegAsync = async (images: ImageType | ImageType[]) => {
    return Array.isArray(images)
        ? Promise.all(images.map(Promise.resolve))
        : Promise.resolve(images);
};
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Hi @CertainPerfomance. Thanks for the help! I tried your second option but `map` still complains. `any Property 'map' does not exist on type 'ImageType | (ImageType | ImageType[])[]'. Property 'map' does not exist on type 'ImageType'.ts(2339)` – GollyJer Jan 09 '20 at 00:43
  • The `Array.isArray` check *should* take care of it - it will narrow down the type to either an `ImageType` or an `ImageType[]`. What TS version are you using? It works fine for me on TS 3.7.3 – CertainPerformance Jan 09 '20 at 00:45
  • Yup! That was it. I didn't realize that was a key part of your answer. When I change my version to `const imagesToConvert = Array.isArray(images) ? images : [images];` it works. – GollyJer Jan 09 '20 at 00:50
  • The construction of the `imagesToConvert` array shouldn't be necessary, it makes things pretty verbose, since you have to conditionally convert to an array and then conditionally convert it back again - once you have a function which converts an image, I think the (much more consise) code in the answer is probably a better choice – CertainPerformance Jan 09 '20 at 00:54
1

Unfortunately, that cannot be done because this is a runtime check. However, you can use type-guard to achieve what you want:

type ImageType = {
  uri: string;
  type?: string;
  height?: number;
  width?: number;
};

type ConvertImagesToJpegParams = {
    (...param: ImageType[]): Promise<ImageType[] | ImageType>;
};


// Type guard for an array response
const isArray = <T extends object>(images: T | T[]): images is T[] => {
    return Array.isArray(images);
}


// Type guard for an object response
const isObject = <T extends object>(images: T | T[]): images is T => {
    return !Array.isArray(images);
}

const convertImagesToJpegAsync: ConvertImagesToJpegParams = async (...images) => {
    if (images.length === 0) {
        // edge case; should probably throw an exception
    }

    const convertedImages: ImageType[] = [];
    const promises = images.map(async (image) => {
        // do stuff here
    });

    await Promise.all(promises);

    return images.length === 1
        ? convertedImages[0]
        : convertedImages;
};

// And now to use it

const result = await convertImagesToJpegAsync();
if (isObject(result)) {
   // now result.uri exists        
}
if (isArray(result)) {
  // now result.forEach exists
}
Kousha
  • 32,871
  • 51
  • 172
  • 296
  • This doesn't narrow the type of the return value, unfortunately – CertainPerformance Jan 09 '20 at 00:56
  • It still requires the caller of `convertImagesToJpegAsync` to do a runtime check just to get the type of the response, which seems not very desirable – CertainPerformance Jan 09 '20 at 01:12
  • Yes exactly. The problem is, Typescript is a compiler check, and what the OP is asking is a runtime check. – Kousha Jan 09 '20 at 01:13
  • Thanks Kousha. I learned a lot from your answer! Gave @CertainPerfmance the checkmark though because it works as intended. Ultimately I ended up creating two separate functions as this just kept getting smellier and smellier. – GollyJer Jan 09 '20 at 01:57