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?
CodePudding user response:
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([]);
}
}
(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
);
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:
- Define two separate methods instead,
popOne
andpop
/popSome
or similar. - 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