zurfyx
zurfyx

Reputation: 32767

TypeScript Building a Promise.all like function - Overload signature is not compatible with function implementation

By using TypeScript, anyone using sequentially should be forced to pass 1 to N parameters, which input Promise<T> is equal to the output [Promise<T>,..

[Not working] I'm getting tslint errors in the first declaration when overloading it in the following way:

Overload signature is not compatible with function implementation. ts(2394)

promise.ts

function sequentially<T1>(promiseWrappers: [
  () => Promise<T1>
]): Promise<[T1]>;
async function sequentially<T1, T2>(promiseWrappers: [
  () => Promise<T1>,
  () => Promise<T2> 
]): Promise<[T1, T2]> {
  const resolved = [];
  for (const wrapper of promiseWrappers) {
    resolved.push(await wrapper());
  }
  // @ts-ignore
  return resolved;
}

[Working] Only way that seems to be working is when writing the implementation with .js and a separate declaration file .d.ts, like the following:

promise.js

export async function sequentially(promiseWrappers) {
  const resolved = [];
  for (const wrapper of promiseWrappers) {
    resolved.push(await wrapper());
  }
  return resolved;
}

promise.d.ts

export declare function sequentially<T1, T2>(promiseWrappers: [
  () => Promise<T1>,
  () => Promise<T2>
]): Promise<[T1, T2]>;

export declare function sequentially<T1, T2, T3>(promiseWrappers: [
  () => Promise<T1>,
  () => Promise<T2>,
  () => Promise<T3>
]): Promise<[T1, T2, T3]>;

How do I fix the first TS implementation case?

Upvotes: 3

Views: 471

Answers (2)

artem
artem

Reputation: 51619

There are some rules for implementing overloaded functions in TypeScript:

  1. Implementation signature is not taken into account at the call sites, it's used only for typechecking the implementation (mentioned at the end of the paragraph documenting overloads in the handbook)

  2. Implementation signature must have parameters which are compatible with all overloaded signature declarations. In practice, it means that each parameter type must be a union type of all parameter types from all overloads for that parameter position.

  3. Implementation signature must have return type which is an intersection of return types from all overloads. In practice, it means that the implementation must do type cast to that type in each return statement, or simply have implementation return type declared as any.

I can't find 2 and 3 documented anywhere, but they follow from general variance rules for function type compatibility and the fact that implementation signature must be compatible with all overloaded declarations.

Here is the code, which compiles with --strictFunctionTypes and --noImplicitAny turned on

function sequentially<T1>(promiseWrappers: [
    () => Promise<T1>
]): Promise<[T1]>;
function sequentially<T1, T2>(promiseWrappers: [
    () => Promise<T1>,
    () => Promise<T2>,
]): Promise<[T1, T2]>;
async function sequentially<T1, T2>(promiseWrappers: [
  () => Promise<T1>,
] | [
  () => Promise<T1>,
  () => Promise<T2> 
]): Promise<[T1, T2] & [T1]> {
  const resolved = [] as unknown as [T1, T2] & [T1];
  for (const wrapper of promiseWrappers) {
    resolved.push(await wrapper());
  }
  return resolved;
}

Upvotes: 3

Catalyst
Catalyst

Reputation: 3227

The overloads you may want to support may have nothing to do with eachother. To support this make the bottom-most signature all any types. Something like this should work for you.

function sequentially<T1>(promiseWrappers: [
  () => Promise<T1>
]): Promise<[T1]>;
function sequentially<T1, T2>(promiseWrappers: [
  () => Promise<T1>,
  () => Promise<T2> 
]): Promise<[T1, T2]>;
async function sequentially(promiseWrappers: any[]): Promise<any[]> {
  const resolved = [];
  for (const wrapper of promiseWrappers) {
    resolved.push(await wrapper());
  }
  // @ts-ignore
  return resolved;
}

Upvotes: 2

Related Questions