5

In plain untyped Javascript, it's not so hard to write a function which can operate on either numbers or bigints, depending on the arguments which are passed in:

const sumOfSquares = (a,b) => a*a + b*b;
sumOfSquares(3, 4); // returns 25
sumOfSquares(3n, 4n); // returns 25n
sumOfSquares(3n, 4); // throws a TypeError

It seems like there ought to be a way to declare this function in typescript so that the compiler will enforce that the arguments will work together. I tried

const sumOfSquares = <N extends bigint | number>(a: N, b: N): N =>
  a * a + b * b;

But the compiler rejects this:

semantic error TS2322: Type 'number' is not assignable to type 'N'.
  'number' is assignable to the constraint of type 'N', but 'N' could be instantiated with a different subtype of constraint 'number | bigint'.

Is there a different way to write the type declaration so that it will work?

mlc
  • 1,668
  • 2
  • 16
  • 30
  • You can always try an overload, although it's not as clean as the generic. – Bergi Dec 13 '20 at 21:26
  • I think you cannot even mix bigint and number, e.g. 1n+2 is invalid, so what is the point of mixing types? – ABOS Dec 13 '20 at 21:29
  • [The overload I thought of](https://www.typescriptlang.org/play#code/MYewdgzgLgBBCuBbA8gMwMoEd4EMBOAphAFwwDeAUDNTABQ6lhIBGBeANDM4y2wJQ9ErPAG4qNeqWYBLAObSwUTty5yFUAavmKxAXxgBeOjmV9DAPnE4YAKhjWA1F1tcRQA) doesn't work. Looks like TypeScript support for bigints is spotty - e.g. it still assumes that `*` always returns a number. – Bergi Dec 13 '20 at 21:31
  • @ABOS The intention was that you need to pass either two numbers or two bigints. But you're right, that's what the compiler assumes: you *could* instantiate `N` with the union `number | bigint` (instead of only one of them) and break the function. – Bergi Dec 13 '20 at 21:33
  • @Bergi, I added overloaded version below, it is verbose, but seems working – ABOS Dec 13 '20 at 22:55
  • @mlc I made an update, please take a look – captain-yossarian from Ukraine Dec 14 '20 at 08:53

2 Answers2

0

Here is the solution:

function sumOfSquares<N extends number>(a: N, b: N):N
function sumOfSquares<N extends bigint>(a: N, b: N):N
function sumOfSquares<N extends bigint | number>(a: N, b: N) {
    return a * a + b * b;
}

sumOfSquares(2n,2n) // ok
sumOfSquares(2,2) // ok
sumOfSquares(2n,2) // error
sumOfSquares(2,2n) // error

Btw, you can also define overloadings for arrow function:


interface Overloading {
    <N extends number>(a: N, b: N): N
    <N extends bigint>(a: N, b: N): N
    <N extends bigint | number>(a: N, b: N): N
}

const sumOfSquares: Overloading = <N extends bigint | number>(a: N, b: N) => a * a + b * b;


sumOfSquares(2n, 2n) // ok
sumOfSquares(2, 2) // ok
sumOfSquares(2n, 2) // error
sumOfSquares(2, 2n) // error

If you passed first argument as simple number, TS will expect second argument to have the same type. Same behaviour with BigInt's

UPDATE

I should have beed add one extra generic to make it work.

Thanks @jcalz for pointing me in a right direction:

function sumOfSquares<A extends number, B extends number>(a: A, b: B): number
function sumOfSquares<A extends bigint, B extends bigint>(a: A, b: B): bigint
function sumOfSquares<A extends number | bigint, B extends A>(a: A, b: B): bigint | number {
    return a * a + b * b
};

const x = 3n;
let y: number | bigint;
if (Math.random() < 0.5) y = 4;
else y = 4n;

const result = sumOfSquares(x, y) // There should be an error here
const result2 = sumOfSquares(3n, 4) // There should be an error here too
const result3 = sumOfSquares(3, 4n) // There should be an error here too
const result4 = sumOfSquares(3, 4) // ok
const result5 = sumOfSquares(3n, 4n) // ok
  • 1
    Using the test cases people were complaining about in my answer, your second answer in this post doesn't pass. https://www.typescriptlang.org/play?target=99#code/JYOwLgpgTgZghgYwgAgPIDdoBsD2cAmoA5sgN4BQyVyAPAHLIQAekI+AzsiAK4C2ARtAB8ACjgAuZHQA0yfpLoBKBZWr1GLCG079gRUGFESps+VOVTVVdc1Yc5eg8gA+XPoKhGFphRbrkAX3JyBBwQdjBkdj5UGABlAEduOCgIdkkMbDxCEBIAXloGWy17XX1wFzcBYTFvOV9kPKFkOGQAKhbkAGo5drkAbmDQ8MimRuQAZhBBrAhIgE9JHmqoSrKDQeAYZBEAWTgwAAsAOig4NhxeEUVaZAAGY4BWG-nxgBZBiCx2FFeCt+mwWivFiiWSqXYIiYsnmNwA9HDkAAVQ7QFDsQ44bhYfByFDnRhQKA4Vao1LkYGgpIpNI0ZYeNaOcCiKayN7wxEotFRTHY3GCFogQnE0ncsA4HBAA – Samathingamajig Dec 13 '20 at 22:42
  • Any type system can't figure out the type which is the product of RANDOM – captain-yossarian from Ukraine Dec 13 '20 at 22:49
  • that is my point exactly, see the comments from my answer – Samathingamajig Dec 13 '20 at 23:21
  • 1
    I think you just don't want the third call signature in your `Overloading` interface – jcalz Dec 14 '20 at 01:58
  • @jcalz I have tried. Without third overloading, TS complains about function definition – captain-yossarian from Ukraine Dec 14 '20 at 08:03
0

Does overload work?

function isBigInt(a: bigint | number): a is bigint {
  return typeof (a) === 'bigint'
}

function isNumber(a: bigint | number): a is number {
  return typeof (a) === 'number'
}

function doSomething(a: bigint, b: bigint): bigint;
function doSomething(a: number, b: number): number;
function doSomething(a: bigint | number, b: bigint | number): bigint | number | never {
  if (isBigInt(a) && isBigInt(b))
    return a + b;
  else if (isNumber(a) && isNumber(b))
    return a + b;
  throw 'error';
}

let a = doSomething(1, 1)
let b = doSomething(1n, 1n)
let c = doSomething(1n, 1)
ABOS
  • 3,723
  • 3
  • 17
  • 23