Skip to content

Allow calling union types with different call signatures when arguments are unions of types with at least one type satisfying one of the functions and a type guard narrows the argument union to the correct type at runtime #37223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
5 tasks done
LuminescentMoon opened this issue Mar 5, 2020 · 2 comments
Labels
Duplicate An existing issue was already created

Comments

@LuminescentMoon
Copy link

Search Terms

calling union types

Suggestion

Give TS the ability to infer that calling a value that is typed with a union of functions of differing call signatures is valid when a type guard that narrows the union of functions to a function, or a new union of functions with identical signatures, is used in a conditional expression to select arguments with the correct type to be passed.

Use Cases

Creating a TypedArray using a variable TypedArrayConstructor:

declare function isBigInt64ArrayConstructor(x: any): x is BigInt64ArrayConstructor

let x: Int16ArrayConstructor | Int32ArrayConstructor | BigInt64ArrayConstructor

// From this
function doStuff(y: number) {
  switch (y) {
    case 0:
      if (!isBigInt64ArrayConstructor(x))
        return x.of(0, 1)

      return x.of(0n, 1n)
    case 1:
      if (!isBigInt64ArrayConstructor(x))
        return x.of(1, 0)

      return x.of(1n, 0n)
    case 2:
      if (!isBigInt64ArrayConstructor(x))
        return x.of(0, 1, y)

      return x.of(0n, 1n, BigInt(y))
  }
}

// To this
function cleanerDoStuff(y: number) {
  const [ZERO, ONE, WHY] = isBigInt64ArrayConstructor(x)
    ? [0n, 1n, BigInt(y)]
    : [0, 1, y]

  switch(y) {
    case 0:
      return x.of(ZERO, ONE)
    case 1:
      return x.of(ONE, ZERO)
    case 2:
      return x.of(ZERO, ONE, WHY)
  }
}

Examples

This does not work as of TS 3.7.5:

interface iTakeNumbers {
  (x: number)
  takesBigInts: false
}

interface iTakeBigInts {
  (x: bigint)
  takesBigInts: true
}

type iTakeNumbersOrBigInts = iTakeNumbers | iTakeBigInts

let x: iTakeNumbersOrBigInts

function doesTakeNumbers(x: iTakeNumbersOrBigInts): x is iTakeNumbers {
  return !x.takesBigInts
}

const ZERO = doesTakeNumbers(x) ? 0 : 0n

if (doesTakeNumbers(x)) {
  // Works as TS narrows x to iTakeNumbers
  x(0)
  /**
   * Case 1:
   * Argument of type 'bigint | 0' is not assignable to parameter of type 'number'.
   * Type 'bigint' is not assignable to type 'number'. ts(2345)
   */
  x(ZERO)
} else {
  // Works as TS narrows x to iTakeBigInts
  x(0n)
  /**
   * Case 2:
   * Argument of type 'bigint | 0' is not assignable to parameter of type 'number'.
   * Type '0' is not assignable to type 'number'. ts(2345)
   */
  x(ZERO)
}

/**
 * Case 3:
 * Argument of type 'bigint | 0' is not assignable to parameter of type 'never'.
 * Type 'bigint' is not assignable to type 'never'. ts(2345)
 */
x(ZERO)

In the 1st and 2nd case, ZERO should be narrowed down to the appropriate type as the compiler should be able to infer that doesTakeNumbers is pure and thus defining ZERO with a conditional expression at the top level is equivalent to defining ZERO inside both the if and else blocks with the appropriate 0 for each respectively.

In the 3rd case, the compiler should be able to infer that ZERO will be the correct type for use as the argument to x during runtime since ZERO should be conditionally typed correctly based on the result of the type guard doesTakeNumbers called with x. Maybe this could be done with an extension to the existing conditional types where the compiler could infer the type of ZERO to be a conditional type instead of a bigint | number,

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@jcalz
Copy link
Contributor

jcalz commented Mar 5, 2020

Related to or duplicate of #12184; you want to propagate control flow analysis in a way not currently supported. Every time I think about this sort of problem I conclude that it would likely be a compiler performance disaster to have it automatically track these correlated possibilities, and #25051 was my attempt at a feature request for manual instead of automatic tracking.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Mar 10, 2020
@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants