Function Overloading in TypeScript (I was wrong)

  Рет қаралды 13,740

Andrew Burgess

Andrew Burgess

Күн бұрын

TypeScript gives you all the power you need to create flexible functions that support multiple call signatures!
shaky.sh

Пікірлер: 39
@raianmr2843
@raianmr2843 Жыл бұрын
Function overloading with different return types isn't actually type safe at all. TS can't enforce the actual return type deduced from the parameters passed. It just relies on the signature you've provided, only giving you the illusion of type safety with zero errors or warnings when there's a bug in your implementation. This is quite literally the same thing as using the `as` keyword and should be discouraged imo. Here's a buggy version of your `maybe` function: function maybe(fnOrP: () => T): T | undefined function maybe(fnOrP: Promise): Promise function maybe(fnOrP: (() => T) | Promise): T | undefined | Promise { if (typeof fnOrP === "function") { try { return fnOrP() } catch { return undefined } } return undefined // TS thinks this is ok // return fnOrP.catch(() => undefined) } main() function main() { // TS thinks `x` is a Promise const x = maybe(Promise.resolve("typescript")) // but in reality it's just undefined console.log(x) } I don't think you should be treating TS as a separate language from JS. In general, don't use the fancy features JS doesn't have natively (e.g., enums, overloading, and so on) because they usually lead to buggy code.
@andrew-burgess
@andrew-burgess Жыл бұрын
Yikes, this is a really great point, I had no idea! Thanks for sharing! edit: gonna pin this comment so people see this.
@tolstoievski4926
@tolstoievski4926 Жыл бұрын
Why enums leads to buggy code ?
@amritkahlon1988
@amritkahlon1988 Жыл бұрын
I just ran into this, so there's no way to overload this right?
@TimRoberts-ve5wb
@TimRoberts-ve5wb 6 ай бұрын
Without the overloads, the consumer of the function has no information about how the variants of inputs relate to the variants of outputs. If you remove the overloads, then you're just left with the single version based solely on the union types. Regardless of how you 'type-up' this function, the bug will still be present, since the entire variant of code that dealt with Promises is missing. All it means by not having the overloaded signatures, is that you have a less comprehendible signature in `function maybe(fnOrP: Promise | (() => string)): string | Promise | undefined`. Regardless of whether you use overloaded signatures or not, you always have to write the completely widened function as the implementation. As such, I look at overloads in the same light as documentation. They have no bearing on runtime, but their presence helps the consumer of the function comprehend the inputs and outputs more easily.
@kbitgood
@kbitgood Жыл бұрын
Function overloading is good for library code where DX is the goal. But it’s horrible to maintain for application code. In that case just use multiple functions. Then you don’t need to throw all those errors.
@kbitgood
@kbitgood Жыл бұрын
In the overload signature you can also rename the params. So it doesn’t have to be “updateOrKey” for one it would be update:Partial and the other would be key:K
@DontFollowZim
@DontFollowZim Жыл бұрын
In the last example there are a couple things you could do to clean it up a bit. 1. If you know the consumers of the function will be using TS and you have those overloads specified, you can just check for the existence of the last argument to know which signature is being used and not need to check the types. 2. If you're not ok with that, at the very least you could (using the final code for reference) combine the `if`s on lines 32 and 33: if (isString(updateOrKey)) { if (!isWidget(widgetOrValue) && isWidget(widget)) { return { ... } } throw "wrong args"; }
@Luxcium
@Luxcium Жыл бұрын
I love to use function overload especially to handle promises vs non promise code it’s probably an edge case but I love to put the logic of handling async code inside the functions and methods I defined instead of in my code where I am using them 😅
@TheYinyangman
@TheYinyangman Жыл бұрын
Option type in Scala is an iterable that can be None - it makes sense because as a developer your know that argument is Optional rather than any variable just being possibly undefined and having to handle it again as possibly undefined.
@vukkulvar9769
@vukkulvar9769 Жыл бұрын
You can even have different argument names to make it clearer. function updateWidget(update: Partial, widget: Widget): Widget; function updateWidget(key: K, value: Widget[K], widget: Widget): Widget;
@alanbloom20
@alanbloom20 Жыл бұрын
This was rather enlightening- had no idea this was a valid typescript pattern :-)
@TheYinyangman
@TheYinyangman Жыл бұрын
My neck gets sore just watching this guy
@andrew-burgess
@andrew-burgess Жыл бұрын
Because you're nodding along so much? Or shaking your head in disagreement?
@Schippo10
@Schippo10 Жыл бұрын
Love your videos ❤ They‘re exactly in the right spot between niche knowledge and things that you can apply to your daily workflows. 🎉
@radulaski
@radulaski Жыл бұрын
Happy New Year, awesome as always, I see many more subscribers in your future :D
@mluevanos
@mluevanos Жыл бұрын
I don't use overloads too much either, but there was this one instance in my Next.js codebase where I neeed to set cookies via server-side or client-side. /** * Set a cookie with options.context: * - Set Document cookie, or * - Set Response Headers cookie */ function set( cookieName: string, value: CookieValue, options?: CookieOptions ): void; function set( this: NextApiResponse, cookieName: string, cookieValue: CookieValue, options: CookieOptions = {} ) { const { expires: days, context = 'document', domain: host = '' } = options; const domain = !host ? '' : `Domain=.${host};`; const expires = !days ? '' : `Expires=${getExpirationDate(days)};`; const value = `${cookieName}=${cookieValue};`; const path = `Path=/;`; const cookie = `${value} ${expires} ${domain}${path}`; if (context === 'document') { document.cookie = cookie; } if (context === 'headers' && 'setHeader' in this) { this.setHeader('Set-Cookie', cookie); } }
@hugodsa89
@hugodsa89 Жыл бұрын
Function overload with intersections is where's at.
@kamilzielinski1303
@kamilzielinski1303 Жыл бұрын
i think it is worth to mention that this is not possible with arrow const functions :( only the old way functions declarations
@AibySara
@AibySara 4 ай бұрын
Great explanation. Big Thanks!
@tomshieff
@tomshieff Жыл бұрын
Amazing content! Thank you, I'm subscribing right now
@edgarabgaryan8989
@edgarabgaryan8989 Жыл бұрын
Realy like your content
@iamrohandatta
@iamrohandatta Жыл бұрын
I believe it is possible to make TS smart enough to not require all those checks inside the updateWidget function. What you can do is define the two possible argument signatures as two named tuple types (say Sig1Params , Sig2Params). Then you can use: updateWidget(...args: Sig1Params | Sig2Params) {...} Now you only need one check for TS to narrow down the types of the args. Sorry i can't produce a playground link as I'm writing this from my phone. But let me know if you need more clarification, i can get back later and share a playground link.
@vukkulvar9769
@vukkulvar9769 Жыл бұрын
I tried it, and it did not work. args[0] is typed K|Partial args[1] is typed Widget[K]|Widget args[2] is typed Widget|undefined
@dealloc
@dealloc Жыл бұрын
@@vukkulvar9769 You have to use tuples such that the signature is as follows: function doSomething(arg1: string, arg2: number, arg3: Date): string; function doSomething(arg1: string, arg2: Date): string; function doSomething(arg1: number): string; function doSomething(...args: [string, number, Date] | [string, Date] | [number]); If each overload has different lengths, you could just check the length of args to determine which tuple to use. TS will correctly narrow down the type. If you have two overloads with the same number of arguments but different type signatures, you can narrow down the args further with a type guard. TS won't be able to narrow the type of the args tuple if you only check the type of individual parameters, unfortunately.
@vukkulvar9769
@vukkulvar9769 Жыл бұрын
@@dealloc Ok, thanks for the insight
@kappasphere
@kappasphere Жыл бұрын
Doesn't this also solve the messy code inside your updateWidget function? Because there are only two possibilities instead of eight, you also need only one binary check instead of three. Just test if the third argument is a widget and if it is, you return { ...widget, [updateOrKey]: widgetOrValue}, else you return {...widgetOrValue, ...updateOrKey}. You don't need any of the "wrong args" cases anymore because they're all already caught by the type checking.
@kappasphere
@kappasphere Жыл бұрын
Okay I just tried it out, and it looks like Typescript isn't smart enough to figure out that a single argument can act as the discriminator of the type signature. I still wouldn't nest the "if" statements like this, I'd just assert the values of all three arguments and when no case matches, throw an exception.
@andrew-burgess
@andrew-burgess Жыл бұрын
Yep, TS needs you to handle the implementation signature, even though it won’t accept that as a valid way to call the function. And yeah, I agree, multiple if-statements was unnecessary here 👍
@dealloc
@dealloc Жыл бұрын
@@kappasphere If your overload signatures has different arg lengths of different types, you can use a union of tuples as the args type on the function signature and then use .length to determine which overload to use: function doSomething(arg1: string, arg2: number, arg3: Date): string; function doSomething(arg1: string, arg2: Date): string; function doSomething(arg1: string): string; function doSomething(...args: [string, number, Date] | [string, Date] | [string]): string { if (args.length === 1) { return args[0]; } else if (args.length === 2) { const [a1, a2] = args; return a1 + a2.toISOString() } const [a1, a2, a3] = args; return a1 + a2 + a3.toISOString() } If you have an overload with the same number of args, you could use a custom type guard to narrow down the args type based on the type of some positional argument. Typeof won't for that case since TS cannot infer type of "args" based on the type of a single element in the tuple for example, as you're narrowing the individual type of the tuple element, rather than the total sum type of args.
@noahwinslow3252
@noahwinslow3252 Жыл бұрын
Great video! I didn't know about this!
@SimonCoulton
@SimonCoulton Жыл бұрын
This is excellent, thanks!
@DaveTheDeveloper
@DaveTheDeveloper Жыл бұрын
I hate function overloading in ts. It is neat when using it but ugly implementing it. Better implementations of this paradigm can be found in other languages like C#
@vytah
@vytah Жыл бұрын
That's because it's not actually overloading anything, it's creating a complex conditional function signature that cannot be typechecked and leaves it to the programmer to manually verify that the conditions are satisfied.
@redcrafterlppa303
@redcrafterlppa303 Жыл бұрын
The more I watch your videos the less I like typescript. This doesn't mean your videos are bad. It's the opposite. Your videos show a broad look at typescript. This broad look helps me to understand that things are done in a stupid way. It's the same for python. Things are done in a different often worse way then most languages. For example overloading. Would it have been so difficult to simply supply the standard model of overloading with seperate bodies? No they do it differently with this weird shit that doesn't solve the problem overloading was designed to solve. To have different implementations for the same function name. You have to then check the types of the overload with if cases in the body. This is like you would sieve sand and then throw the things collected back on the pile and pick them back out by hand.
@Spice__King
@Spice__King Жыл бұрын
The short answer is no. The long answer is that Typescript functionally stripped out when compiled to JS, including the overload definitions, leaving just the single function body. If you want multiple bodies, write multiple separate functions as JS would have no clue which updateWidget to call and TS is not going to automatically write out code to typecheck overloads and pick the right sub body to a function. Would be nice, but not in the plans for TS, in which the only thing not 100% compiled out is Enums and they regret adding those. I do agree that TS could be smarter about args, like function overloads that get a bit shafted for some build in things like Parameters type, so there is room to grow.
@redcrafterlppa303
@redcrafterlppa303 Жыл бұрын
@@Spice__King on the first hand it seems like this but instead of just erasing the types TS could simply mangle the overloads with the parameter types and resolve the callsite to use the mangled function. This is how function overloading unusually works. A function set : function add(a: number, b number): number {} function add(a: string, b string): string {} Would be converted to: function add_number_number(a: number, b number): number {} function add_string_string(a: string, b string): string {} Such resolution is 100 percent doable by the typescript compiler js code could also call it since it would see the mangled names as separate functions. It was simply a choice of the typescript team. (In my opinion one of the many bad ones).
@Spice__King
@Spice__King Жыл бұрын
@@redcrafterlppa303 any mangling runs counter to Typescript's own design goals. The one thing that does not get fully stripped out, Enums, is one of the things they regret and would opt to not implement again if given a clean slate. Mangling would also throw issues with compiled TS libraries being used in JS. You've just created a compiler controlled name to a number of extra functions. The TS project does not want that, they want JS but with compiled out type checking that would interoperability with TS just fine on both ends of the interoperability. If JS gets some kind of native type system, I'd expect the TS team would adopt the features it provides in time.
@vytah
@vytah Жыл бұрын
@@redcrafterlppa303 There are tons of places where you can have a call to add that cannot be resolved at compile time. The most obvious ones involve 'any', but also generics like 'T extends string | number'. Also, this would require the signatures to be visible when compiling any piece of code mentioning add, which would prevent the possibility of adding overloaded signatures to external JS libraries - the very reason this feature was even added to TS in the first place.
How to use TypeScript Enums and why not to, maybe
12:43
Andrew Burgess
Рет қаралды 19 М.
TypeScript Generics are EASY once you know this
22:21
ByteGrad
Рет қаралды 137 М.
Cute
00:16
Oyuncak Avı
Рет қаралды 12 МЛН
小丑在游泳池做什么#short #angel #clown
00:13
Super Beauty team
Рет қаралды 43 МЛН
How To Get Married:   #short
00:22
Jin and Hattie
Рет қаралды 15 МЛН
Expected Ending?
00:45
ISSEI / いっせい
Рет қаралды 11 МЛН
Learn Web Development And ACTUALLY Get A Job | Ultimate Guide
1:33:52
James Cross
Рет қаралды 1,3 МЛН
No BS TS #4 - Function Overloading in Typescript
10:01
Jack Herrington
Рет қаралды 37 М.
How to use generics in TypeScript
11:46
Andrew Burgess
Рет қаралды 37 М.
#12 - Functions in TypeScript || Anonymous Function
17:27
Naveen AutomationLabs
Рет қаралды 2,1 М.
Branded Types give you stronger input validation
9:22
Andrew Burgess
Рет қаралды 17 М.
TypeScript Wizardry: Recursive Template Literals
14:47
Tech Talks with Simon
Рет қаралды 37 М.
Enums considered harmful
9:23
Matt Pocock
Рет қаралды 206 М.
Cute
00:16
Oyuncak Avı
Рет қаралды 12 МЛН