1

Imagine a simplified version of my requirements: I pass an array of items to a function, a callback and a number of how many items I want to pop from the array. The callback will get that number of items.

If the pop count is set to 1, I want that callback to receive only that single item. If it's anything else from 1, I want it to pass an array to the callback.

I'm not sure if it's possible in TypeScript. I've been playing around without success yet. Here's what I came up with (and doesn't work):

function pop<T>(items: T[], cb: (item: T) => void, count: 1): void;
function pop<T>(items: T[], cb: (item: T[]) => void, count: undefined): void;
function pop<T>(items: T[], cb: (item: T[]) => void, count: number): void;
function pop<T>(
  items: T[],
  cb: ((item: T) => void) | ((item: T[]) => void),
  count = 1,
): void {
  if (count === 1) {
    cb(items[0]);
  } else if (count > 1) {
    cb(items.slice(0, count));
  } else {
    cb([]);
  }
}

Can anyone enlighten me whether this is possible at all? Or whether I'm missing something?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Jasper Kuperus
  • 1,612
  • 1
  • 17
  • 22
  • The overloads are for the _callers_; inside the actual implementation `cb` isn't conditionally typed, it's always `((item: T) => void) | ((item: T[]) => void)`. Note you can squash the second and third overloads into `count?: number`. – jonrsharpe Nov 01 '21 at 09:46

1 Answers1

3

It is possible, you just have to loosen the implementation signature (which callers don't see, they only see the overload signatures):

function pop<T>(items: T[], cb: (item: T) => void, count?: 1): void;
function pop<T>(items: T[], cb: (items: T[]) => void, count: number): void;
function pop<T>(
    items: T[],
    cb: (items: T | T[]) => void,
    count = 1,
): void {
    if (count === 1) {
        cb(items[0]);
    } else if (count > 1) {
        cb(items.slice(0, count));
    } else {
        cb([]);
    }
}

Playground link

(Since the default for count was 1, I've folded it into the first overload signature by making count optional when its type is 1.)

Beware though that TypeScript will assume any non-literal (or at least not immediately-obviously-constant) number you pass as count means that the callback expects an array, since the type is number, not 1. So this overload only works to provide the callback with the item (rather than an array) if you specify 1 literally (or as an immediately-obviously-constant) in the call.

Examples:

declare let items: number[];

pop(
    items,
    (item) => {
        console.log(item); // type is `number`
    },
    1
);

let count = 1;
pop(
    items,
    (item) => {
        console.log(item); // type is `number[]`, not `number`
    },
    count
);

Playground link

Even not the type of item is number[], at runtime it will receive a single number, not an array, because the runtime code just knows that the count parameter is 1, not why it's 1. As Jörg W Mittag points out in the comments, this is because the overloads are purely a compile-time / type-checking thing in TypeScript; the only part that actually happens at runtime is the JavaScript implementation, which doesn't know about the static types. (This is in contrast to languages like Java where the overloads are literally separate functions and the specific one being called is determined at compile-time, not runtime.)

You can fix that in a couple of ways:

  1. Define two separate methods instead, popOne and pop/popSome or similar.
  2. Require that const only be a literal or compile-time constant.

#1 is self-explanatory, but captain-yossarian shows us how to do #2, via an OnlyLiteral generic type:

type OnlyLiteral<N> = N extends number ? number extends N ? never : N : never;

Then the second overload signature is:

function pop<T>(items: T[], cb: (items: T[]) => void, count: OnlyLiteral<number>): void;

...and the case giving us number[] in my earlier example becomes a compile-time error: playground link

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 1
    it is possibe to forbid `non-literal numbers`. COnsider this example: `type OnlyLiteral = N extends number ? number extends N ? never : N : never` – captain-yossarian from Ukraine Nov 01 '21 at 09:57
  • @captain-yossarian - Ooooooh, that's cool!! I'll add it. – T.J. Crowder Nov 01 '21 at 10:04
  • With regards to your caveat, I think it is important to remember that overloading is a *static* (compile time) match on *type*, not a *dynamic* (runtime) match on *value*. Which in TypeScript is actually confusing because the overload only happens in the signature, whereas the actual *implementation* of the overload behavior is of course at runtime based on the value, since there is no such thing as a static type in ECMAScript. – Jörg W Mittag Nov 01 '21 at 10:05
  • Whereas in languages with overloading resolution as part of the language semantics, the overload is resolved by the compiler at compile time based on the type. – Jörg W Mittag Nov 01 '21 at 10:06
  • Agreed, good points to clarify @Jörg. :-) I've added a bit of that in. – T.J. Crowder Nov 01 '21 at 10:08
  • Case in point: https://stackoverflow.com/q/69796947/2988 – Jörg W Mittag Nov 01 '21 at 12:30
  • 1
    Thanks! Looks like I was almost there with my callback signature. Anyway, the caveat with the literal made me rethink my approach. Thanks a lot for the brilliant (and fast!) answer! – Jasper Kuperus Nov 01 '21 at 13:17