1

I have a generic function with a required transformer generic function parameter and a generic return type T.

I want to add a "default value" for that transformer parameter that has a concrete type (ArrayBuffer).

I think I've written the overload signatures correct, but my implementation does not compile.

export async function downloadWithCache(
  url: string
): Promise<ArrayBuffer>;
export async function downloadWithCache<T>(
  url: string,
  transformer: (buffer: ArrayBuffer) => T
): Promise<T>;
export async function downloadWithCache<T>(
  url: string,
  transformer?: (buffer: ArrayBuffer) => T
) {
  return someImplementation(url, transformer || (x => x));
}
Argument of type '((buffer: ArrayBuffer) => T) | ((x: ArrayBuffer) => ArrayBuffer)' is not assignable to parameter of type '(buffer: ArrayBuffer) => T'.
  Type '(x: ArrayBuffer) => ArrayBuffer' is not assignable to type '(buffer: ArrayBuffer) => T'.
    Type 'ArrayBuffer' is not assignable to type 'T'.
      'T' could be instantiated with an arbitrary type which could be unrelated to 'ArrayBuffer'.ts(2345)

P.S. I think that someImplementation is irrelevant, but I was asked to post it:

const httpGetDataWithCache = async <T>(
  url: string,
  transformer: (buffer: ArrayBuffer) => T,
  cacheName: string = "cache",
  updateIfInCache: boolean = false
): Promise<T> => {
  const cache = await caches.open(cacheName);
  let response = await cache.match(url);
  let needToUpdateCache = false;
  if (response === undefined || updateIfInCache) {
    try {
      const newResponse = await fetch(url);
      if (!newResponse.ok) {
        throw new Error(
          `Network response was not OK: ${newResponse.status}: ${newResponse.statusText}`
        );
      }
      response = newResponse;
      needToUpdateCache = true;
    } catch (err) {
      if (response === undefined) {
        throw err;
      }
    }
  }
  // Preventing TypeError: Failed to execute 'put' on 'Cache': Response body is already used
  const responseForCaching = response.clone();
  // Need to verify that the transformer executes with error before putting data in cache.
  const result = transformer(await response.arrayBuffer());
  if (needToUpdateCache) {
    await cache.put(url, responseForCaching);
  }
  return result;
};

const someImplementation = httpGetDataWithCache;
Ark-kun
  • 6,358
  • 2
  • 34
  • 70
  • please share `someImplementation` – captain-yossarian from Ukraine Jun 28 '22 at 09:10
  • @captain-yossarianfromUkraine I really think it's irrelevant... But I've now added it. – Ark-kun Jun 28 '22 at 09:14
  • 1
    @Ark-kun the implementation of someImplementation is irrelevant, but the type signature of someImplementation is relevant – mbdavis Jun 28 '22 at 09:44
  • You need to loose strictness a bit inside your function body. See [here](https://tsplay.dev/wjnevw) . It is safe, because inner body of function treats callback as `(buffer: ArrayBuffer) => ArrayBuffer` whereas outter function representation treats it as `(buffer: ArrayBuffer) => T` – captain-yossarian from Ukraine Jun 28 '22 at 10:02
  • Hmm. Interesting. It seems really weird that the overload with `transformer: (buffer: ArrayBuffer) => T` does not conflict with the implementation signature `transformer: (buffer: ArrayBuffer) => ArrayBuffer` despite `T` not being a descendant of `ArrayBuffer`. I thought that the implementation signature must be compatible with all overload signatures type-wise. Thank you. – Ark-kun Jun 29 '22 at 03:53

1 Answers1

3

You need to change function type signature:

declare const someImplementation: <T>(
    url: string,
    transformer: (buffer: ArrayBuffer) => T,
    cacheName?: string,
    updateIfInCache?: boolean
) => Promise<T>

export async function downloadWithCache(
    url: string
): Promise<ArrayBuffer>;
export async function downloadWithCache<T>(
    url: string,
    transformer: (buffer: ArrayBuffer) => T
): Promise<T>;

export async function downloadWithCache(
    url: string,
    transformer?: (buffer: ArrayBuffer) => ArrayBuffer // CHANGE IS HERE
) {
    return someImplementation(url, transformer || (x => x));
}

downloadWithCache('a', (x) => x.byteLength) // ok, Promise<number>

Playground

It works because function overloads are bivariant.

See simplified example:

declare let a: (buffer: ArrayBuffer) => ArrayBuffer
declare let b: <T, >(buffer: ArrayBuffer) => T

a = b // ok
b = a // error

At least one operation should be ok to make overloadings work.

See docs

The implementation signature must also be compatible with the overload signatures.

In our case, implementation is compatible with overload signature, but not vice versa