CMCDragonkai
CMCDragonkai

Reputation: 6372

Overloaded Typescript Signature with a later parameter type changing based on first parameter type

I thought it would be possible to use typescript overloaded function type signatures to express a situation where the 3rd parameter type changes when the 1st parameter type changes.

type ActionAsync<S> = {
  (order: boolean, seek: S, limit: number): Promise<number>
  (order: null, seekAfter: S, seekBefore: S): Promise<number>
};

async function foo() {
    return 5
}

const x: ActionAsync<Date> = async (order: boolean|null, seekOrSeekAfter: Date, limitOrSeekBefore: number | Date) => {
    if (order === null) {
        return foo();
    }
    if (order === true) {
        return limitOrSeekBefore + 43;
    } else if (order === false) {
        return foo();
    }
}

You can see that limitOrSeekBefore should be number if the order is boolean, but should be S or Date when order is null.

However this doesn't typecheck. It only does if I change to limitOrSeekBefore: any.

Upvotes: 0

Views: 56

Answers (1)

jcalz
jcalz

Reputation: 328473

If by "it doesn't type check" you mean that the compiler doesn't understand that order === true implies that limitOrSeekBefore is a number, then this is just a design limitation in TypeScript. If you assign an arrow function to a variable with multiple call signatures, the compiler does not perform control flow analysis for each call signature inside the implementation. It just does one pass based on the implementation signature, and since order and limitOrSeekBefore are uncorrelated union types there, the compiler cannot do much to verify anything for you.

The canonical issue for this is microsoft/TypeScript#38622, where the word-of-tech-lead is

The only way to detect that this is a valid assignment is to read the function body and understand that it behaves correctly in each overload case; it is far beyond TS's capabilities to do this.

Note that in your case the same problem happens with "normal" overloads:

async function y(order: boolean, seek: Date, limit: number): Promise<number>;
async function y(order: null, seekAfter: Date, seekBefore: Date): Promise<number>;
async function y(order: boolean | null, seekOrSeekAfter: Date, limitOrSeekBefore: number | Date) {
  if (order === null) {
    return foo();
  }
  if (order === true) {
    return limitOrSeekBefore + 43; // error
  } else {
    return foo();
  }
}

The compiler only does control flow analysis based on the implementation signature, not the call signatures. Normal overloads are a bit looser in that you can return a value that is only applicable to some of the call signatures, but since your return value is Promise<number> in all cases, there's no difference in behavior here.


To make it work, I suggest that you use a type assertion to tell the compiler that you know that limitOrSeekBefore is a number and that it shouldn't worry:

const x: ActionAsync<Date> = async (order: boolean | null, seekOrSeekAfter: Date, limitOrSeekBefore: number | Date) => {
  if (order === null) {
    return foo();
  }
  if (order === true) {
    return limitOrSeekBefore as number + 43;
  } else {
    return foo();
  }
}

Playground link to code

Upvotes: 1

Related Questions