Settings

Theme

Functional Programming with TypeScript's Type System

desislav.dev

90 points by evolveyourmind 3 years ago · 45 comments

Reader

foepys 3 years ago

I don't get TypeScript's type system.

It is obviously very powerful and can model very complex type constraints.

But then you have stuff like this where it is not checking types as I would expect:

  interface Foo { bar: string; }
  const f = {bar: "foobar"} as Readonly<Foo>;
  function someFunc(): Foo {
    return f; // No error or warning, even with all strict flags enabled
  }
  • sakesun 3 years ago

    This is an issue open for discussion since 2017

    https://github.com/microsoft/TypeScript/issues/13347

  • globuous 3 years ago

    Yeah, there are some weird stuff in typescript, for instance, this typechecks

        class Animal {}
    
        class Dog extends Animal {
            woof() {}
        }
    
        class Cat extends Animal {
            meow() {}
        }
    
        let append_animals = (animals: Animal[], animal: Animal) => animals.push(animal)
    
        let dogs = [new Dog()]
        append_animals(dogs, new Cat())
    
        dogs.map(dog => dog.woof())
    
    
    Which if you evaluate, you'll obviously get:

        Uncaught TypeError: dog.woof is not a function
    
    Whereas Mypy won't typecheck the equivalent Python code:

        class Animal:
            pass
    
        class Dog(Animal):
            pass
    
    
        def append_animals(animals: List[Animal], animal: Animal) -> None:
            animals.append(animal)
    
    
        dogs = [Dog()]
        append_animals(dogs, Dog())
    
    
    It'll throw with:

        $ mypy types.py 
        types.py:16: error: Argument 1 to "append_animals" has incompatible type "List[Dog]"; expected "List[Animal]"
        types.py:16: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
        types.py:16: note: Consider using "Sequence" instead, which is covariant
    • brabel 3 years ago

      The problem with your TS code is that it's using covariance on a mutable generic type, which is unsafe and strict type systems would've forbidden that.

      To expand: TS treats `Dog[]` as a subtype of `Animal[]` because `Dog` is a subtype of `Animal`... that work if you only read values from the array... but trying to change the array, you run into trouble. Some languages let you declare covariance (reading ) and contravariance explicitly to address this issue. To my limited knowledge of TS, that's not possible in TS (as it tries to keep things simple and compatible with JS, probably).

      The answers in this[1] SO question explain these concepts better than I could.

      [1] https://stackoverflow.com/questions/27414991/contravariance-...

      • DangitBobby 3 years ago

        Why was the `dogs` array initialized as an `Animal[]` type instead of `Dog[]` type which would forbid the addition of a `Cat` type?

        Why would you be able to map a call to `woof` over an `Animal[]` when `Animal` doesn't implement `woof`? I don't understand how the SO link answers these questions.

        • c-hendricks 3 years ago

          > Why was the `dogs` array initialized as an `Animal[]` type instead of `Dog[]`

          That might be your confusion: it wasn't. Its type is `Dog[]`.

          > which would forbid the addition of a `Cat` type?

          Why would that be forbidden? The problematic method is `append_animals`, which only cares that both arguments satisfy `Animal`, which both `Dog` and `Cat` do.

          > Why would you be able to map a call to `woof` over an `Animal[]` when `Animal` doesn't implement `woof`

          Back to your root confusion, since for all intents and purposes, `dogs.map` thinks it's an array of dogs, it doesn't complain.

          If `append_animals` was written like this, things would be fine:

              let append_animals = <T extends Animal>(animals: T[], animal: T) => animals.push(animal)
    • Shacklz 3 years ago

      From my point of view, this is one of those cases where structural typing just doesn't work all that well when used for OOP.

      Typescript does give you a solution to this problem, namely that you use generics to constrain the parameters of your method:

          let append_animals = <T extends Animal>(animals: T[], animal: T) => animals.push(animal)
      
      Your example now gives the expected error.

      TypeScript does indeed have its quirks, but most of them do not really matter for real-life purposes or can easily be worked around like in the example above.

  • _fat_santa 3 years ago

    Having used TS in production with web and mobile (React Native) apps, most of it is rather simple interfaces to make sure you are passing data correctly. I'd say 99.9% of TS code I saw was to make sure that you passed SuperComplexBusinessObject correctly.

    But like you, I've come across many instances where I got "hey TS aren't you supposed throw an error here?"

  • rektide 3 years ago

    Are there other examples of ways TS confounds you?

    This feels like a "haha, gotcha!" moment. Like Gary Bernhardt's Wat talk, it's one single example that looks extremely silly... but has next to no actual impact on anyone using the language regularly, is like the faintest little quirk.

    Typescript seems reasonably acceptable. I struggle to think of what I would ask for, what would be significantly massively different in my life if there were a hypothetical much better alternative to Typescript.

    • deathtrader666 3 years ago

      >> if there were a hypothetical much better alternative to Typescript. Like ReScript ?

    • foepys 3 years ago

      Don't get me wrong, I am very glad that TS exists and I am using it at least weekly. But there are inconsistencies in the type system that are very surprising to beginners because TS is trying so hard to be good at edge cases.

  • rendall 3 years ago

    Sorry, I don't get it. What do you expect to happen here?

    • PhilipRoman 3 years ago

      Presumably the issue is that a Readonly<Foo> shouldn't be a subtype of Foo

      I should note that I haven't yet had the pleasure of using a language that handles const-ness properly, as Readonly<T> should be neither a subtype nor a supertype of T

      • rendall 3 years ago

        As an aside, I'm on mobile and tried to visit the TypeScript playground to play around with this, but weirdly, the default code is an implementation of FizzBuzz! There was not an obvious way to clear it to get a blank editor. Even "select all" context menu was hijacked. So I gave up. I'll have to file an issue.

        • eyelidlessness 3 years ago

          What I do is delete all but a few characters after “code” in the URL, then reload to get an empty playground. It’s annoying but it works.

  • LaGuardiaKid 3 years ago

    Handling of readonly correctness is definitely something ts doesn't handle well

Waterluvian 3 years ago

While no actual Turing machine’s tape is infinitely long, I found issues in TypeScript with how finite generics are.

You have to define every possible count of generic arguments if you want to preserve their types. And if you go above that count your type system degrades. I think there’s also a maximum of 7 or so before it doesn’t work. Beyond that and the generic type widens.

For example, Lodash enumerating types for 2 to 7 generic items per function: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0452...

Admittedly I don’t understand the problem space well. I’ve just seen it happen to me and in others’ code. It might not actually be an issue, or is already fixed.

nkrisc 3 years ago

As wonderfully absurd as this is, I learned more about TypeScript’s type system from this post than I have from its documentation.

Entirely possible that PEBKAC, but I’ve found TypeScript’s documentation to be on the worse end of the programming language documentation quality spectrum.

  • wooly_bully 3 years ago

    It’s good for reference but not for discovery. If I already know the general concept (let’s say Template Literal Types) I can get good info on it, but if I start with a question like ‘Is there a way to make sure this string literal starts with “id_”?’ then I find it very hard to know.

    Random but this is what I’m finding GPT-4 best at: translating random questions into domain terminology + providing examples.

    • iudqnolq 3 years ago

      maybe this is just an issue with me but I've not found any way to search the docs. the only thing Google seems to index is the release notes, and going backwards from the release notes to guess the appropriate section of the docs that will explain that concept is really annoying.

      • nkrisc 3 years ago

        I can’t even find basic type definitions for standard JS functions. I have to type the function in my IDE and then open the type definition from there.

        So it works, I guess, but that’s not really how I want to work.

  • DangitBobby 3 years ago

    It's not just you. To learn how to express more advanced types (or learn whether they are even possible to express), I've had to Google, read source code, or scan random medium articles and blogs from tech companies. Rarely have I learned anything new from the TS docs.

    • nkrisc 3 years ago

      I learned more from this type definition than anything I’ve ever read about TypeScript’s type system before:

        interface add extends F { out: this['args'] extends   [infer a, infer b]     ?
        a extends Zero         ? b :
        a extends Suc<infer n> ? Suc<apply<add, [n, b]>> : 
      
        never : never
      }
toastal 3 years ago

Can you? Certainly. Should you? Ehhhhh…

The post is experimenting with the type system—which is neat—but before you think you should push this in production (having been in positions to work with heavy-FP’d code), consider an actual FP language if that’s the style you want. The ergonomics are so bad compared to any FP lang→JS option. Currying/partial application, first-class composition/bind/apply, pattern matching not using a ._tag property with a String key, and more are just missing (see: migrating from PureScript/Haskell[0] for fp-ts to see how verbose basic concepts becomes). The other issue is that with TypeScript being multiparadigm and idiomatic TS leaning to imperative/OO due to ergonomics and Java’s legacy on culture/education, there’s a good chance your coworkers/contributors will be expecting a non-FP style which will cause even more friction as you try to justify ‘purity’ to folks that just see a verbose codebase that likely is leaning hard into a lib, quality as it is, like fp-ts which cosplays as Haskell, PureScript, OCaml, Idris, et. al.

[0]: https://gcanti.github.io/fp-ts/guides/purescript.html

ralphc 3 years ago

Is it just me or do other people read things like "covariance on a mutable generic type" and just want to get stuff done? Maybe it's because nowadays I do solo or small team projects but this is why I fled to Elixir, it's mostly dynamically typed and you can gradually get into types when you want it, Elixir is cool with that.

bottlepalm 3 years ago

I’m really interested in this for fun, but it’s still way over my head. And this is someone who has been using typescript daily for years. Would love to read something more long form that builds up to this.

ck45 3 years ago

Related: "TypeScripting the technical interview" https://news.ycombinator.com/item?id=35120084

jcparkyn 3 years ago

I've done my share of weird stuff with TS types before, but I'd never seen the trick of using interfaces to make higher-order functions. Neat.

rdez6173 3 years ago

Fun exploration and exceptionally impractical. Love it.

coneonthefloor 3 years ago

What a monstrosity.

firechickenbird 3 years ago

At this point I'm wondering if the TypeScript type system can be used for dependant types that would allow formal verification of the programs

  • remexre 3 years ago

    That'd require it to be sound and programs to be total: an infinite loop is a proof of anything, and type system unsoundness leads to false proofs

hoelle 3 years ago

Gross. I love it.

ekvintroj 3 years ago

Try to type a flatMap and then we talk.

Keyboard Shortcuts

j
Next item
k
Previous item
o / Enter
Open selected item
?
Show this help
Esc
Close modal / clear selection