#Overloads VS Assertion

29 messages Β· Page 1 of 1 (latest)

hasty turret
#

Hey πŸ‘‹
I have a function with a lot of parameters, making the overloading painful (as in: big chunks of code even before the function's body starts).
I'm considering using assertions instead.
What are the pros and cons?
Is your answer the same for lib-land code and user-land code?

Demo:

thorn scarabBOT
#
sirobg#0

Preview:```ts
// Option A
function f(a: number): string;
function f(a: string): number;
function f(a: number | string): number | string {
if (typeof a === "number") { return "hello"; }
else { return 0; }
}

// Option B
function _f<T extends number | string>(a: T): T extends number ? string : T extends string ? number : never {
...```

wet yarrow
#

overloads are marginally safer, as at least tsc is checking whether the return value is a number | string (whereas with the conditional type return version you could return null as any)

#

in general i recommend not using overloaded functions though, regardless of how you type them. YMMV but personally i find it much easier to use and understand (as well as maintain) APIs where you'd have separate fString and fNumber functions

hasty turret
wet yarrow
#

what do you mean? you could have separate getSurprise and getSurpriseUsingDefaultParser functions

hasty turret
#

It is library code. I would like my user to import a function (the same every time, independently of their choice of using the default or custom parser). So I have to wrap these two function inside a main one that I export, right? If so, its signature would have to be overloaded, right?

wet yarrow
#

well yeah if your requirement is "i want an overloaded function" then you need an overloaded function πŸ˜†

#

i was suggesting adjusting your requirements whenever possible

#

but like i said, YMMV

#

the other non-overloaded option is this, but you may not find the return type satisfying:

thorn scarabBOT
#
mkantor#0

Preview:ts ... function getSurprise<T>( s: string, parseString: | ((s: string) => T) | ((s: string) => number) = defaultParser ) { return parseString(s) } ...

wet yarrow
#

or parseString: (s: string) => T | number = defaultParser i guess is simpler

hasty turret
#

Thank you for your suggestions πŸ™

wet yarrow
hasty turret
#

Thanks for clarifying πŸ™
I understood this when you suggested it the first time, but I'm personally not a big fan of this UX/DX. I'll implement this idea and try it for myself to see if I change my mind though!

Would you happen to know a lib which uses this kind of UX/DX ?

#

A famous one, I mean

wet yarrow
#

not off the top of my head, but i don't really pay that much attention to this kind of thing as a library consumer, and it's hard to think of libraries that don't use a feature (or use it sparingly) in the abstract. i'm also not sure how many "famous" libraries i use or what counts as one πŸ˜†

#

i can say i've been frustrated in the past by libraries that overuse overload signatures, because it makes it hard to abstract over their functions or extract/reuse their types (utilities like Parameters<_> don't really work with overloaded functions)

hasty turret
#

Oooh that's a really good point I didn't think about πŸ€”

#

Thanks for sharing

hasty turret
#

For people reading this later on, here is a discussion under Matt Pockock inital tweet about using any:
https://x.com/eyelidlessness/status/1773394643465781476?s=20

@borisfyi @mattpocockuk One last thought: it’s often possible to break the super dynamic cases down as smaller less dynamic functions, getting back the possibility of safe overloads. That can be a perf tradeoff BUT it’s not always intuitive which version will win (monomorphic often == faster)

scarlet sparrow
#

Going to just move the discussion here.
Yeah both options are not safe, option B obviously is not safe because of the cast, while option A has no protection for your implementation. Eg even though the signature says number argument should return string, you can still return a number and completely explode at runtime.

#

The point about overloads ruining type manipulation with Parameters<T> and ReturnType<T> is also true for both options.

#

The advantage of not splitting a function into two is mainly in that consumer of the function doesn't have to change their imports if they want to change which overload to call

#

But the downsides imo far outweigh that minor advantage for the most part.

#

For application code, I will always suggest splitting; for library code, you'd have to weigh the pros and cons and decide which choice to make.