Description
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.