Function Overloading in TypeScript (I was wrong)

Поділитися
Вставка
  • Опубліковано 1 січ 2023
  • TypeScript gives you all the power you need to create flexible functions that support multiple call signatures!
    shaky.sh
  • Наука та технологія

КОМЕНТАРІ • 39

  • @raianmr2843
    @raianmr2843 Рік тому +15

    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  Рік тому +7

      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 4 місяці тому

      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 Рік тому +22

    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 Рік тому +6

    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

  • @Luxcium
    @Luxcium Рік тому +2

    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 😅

  • @DontFollowZim
    @DontFollowZim Рік тому +3

    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";
    }

  • @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

  • @alanbloom20
    @alanbloom20 Рік тому

    This was rather enlightening- had no idea this was a valid typescript pattern :-)

  • @tomshieff
    @tomshieff Рік тому

    Amazing content! Thank you, I'm subscribing right now

  • @TheYinyangman
    @TheYinyangman Рік тому +1

    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.

  • @AibySara
    @AibySara 2 місяці тому

    Great explanation. Big Thanks!

  • @SimonCoulton
    @SimonCoulton Рік тому +1

    This is excellent, thanks!

  • @vukkulvar9769
    @vukkulvar9769 Рік тому +1

    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;

  • @noahwinslow3252
    @noahwinslow3252 Рік тому

    Great video! I didn't know about this!

  • @edgarabgaryan8989
    @edgarabgaryan8989 Рік тому +1

    Realy like your content

  • @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 Рік тому +1

    i think it is worth to mention that this is not possible with arrow const functions :( only the old way functions declarations

  • @iamrohandatta
    @iamrohandatta Рік тому +1

    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 Рік тому +1

      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 Рік тому +1

      @@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

  • @kappascopezz5122
    @kappascopezz5122 Рік тому +1

    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.

    • @kappascopezz5122
      @kappascopezz5122 Рік тому +1

      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  Рік тому +3

      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 Рік тому +1

      @@kappascopezz5122 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.

  • @dueft4479
    @dueft4479 Рік тому

    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 Рік тому +2

    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 Рік тому +1

      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 Рік тому +1

      @@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 Рік тому +1

      @@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.