Hacker News new | ask | show | jobs
by Smaug123 1393 days ago
Yeah, the author appears to be complaining that Typescript is forcing them to stay honest even while they perform Mad Shenanigans like dynamically constructing types :/ if TS didn't check it, it would be down to your users to report the bugs in production!
1 comments

I don't think it's very fair. As a library developer myself, you try to make you user lives easier, which implies being flexible in what you accept when possible. A couple of examples I struggled with recently:

Documenting https://umbrellajs.com/documentation#addclass. The way I documented it is by opening with a code snippet with many of the possible options (which can also be combined!):

    .addClass('name1')
    .addClass('name1 name2 nameN')
    .addClass('name1,name2,nameN')
    .addClass('name1', 'name2', 'nameN')
    .addClass(['name1', 'name2', 'nameN'])
    .addClass(['name1', 'name2'], ['name3'], ['nameN'])
    .addClass(function(node, i){ return 'name1'; })
    .addClass(function(){ return 'name1'; }, function(){ return 'name2'; })
This is pretty easy to do in plain JS, and of course if you are writing code and using it you just read the first 1-4 lines and know what to do for 99% of the cases, while also noticing there's few "advanced/flexible" ways of using it. How would you even do that in TS?

Then there's a classic initializer in JS that works like this:

    function myLibrary(arg) {
      if (!(this instanceof myLibrary)) {
        return new myLibrary(arg);
      }
      ...
    }
This is very useful to create a library like jquery that you can initialize straight away without needing (but also being able to use) the `new` keyword, just calling it like a function and always ensures it returns an instance. To this day I haven't found a way of doing this in TS.
Yeah I kind of disagree that "being flexible in what you accept when possible" is a benefit to the user. It's just more complexity pushed down to the user that is unnecessary.

In this example, I'm not sure why it's the functions responsibility to support all of these options when the user is perfectly capable of manipulating strings and arrays.

While I agree with you, I have seen practical cases where sensing an array of items in the get parameter of a web server is handled differently, similarly to what the parent comment mentioned.
You can list variants of a function's signature in typescript, but typescript won't help you much with "stringly typed" things (like `.addClass('name1,name2,nameN')`).

Different languages have a grain like wood does. And that subtly directs you by making some things ergonomic and some things difficult to express. I love typescript, but I definitely find it changes the resulting code.

Typescript makes "jQuery style" javascript much more awkward to write, because its harder to type. This is good and bad. I write less scrappy code in typescript - which I think makes it a worse language for quick prototyping. But the tradeoff is that I think its a better language for larger teams / longer lasting projects where functions are read a lot more than they're written.

The actual typescript answer for your API is "don't make your API look like that". Its not always the answer you're looking for.

With "template string literal" types TS has gotten incredible at "stringly typed" APIs (more powerful than just about any other type language in existence in this arena, from what I've seen). People have done incredible things with it and its Turing Complete possibilities (including entire games playable in TS types). With great power comes great responsibility, and just because Typescript can do a lot of it now, doesn't mean that you should do it in Typescript.
Honestly, that's just silly. There's absolutely no reason to accept that many different call styles. Why not just take in an array? As a user I don't find things like this convenient, I find them to be confusing footguns. It's a one liner to split your comma separated string into an array as an end-user, but once you add that complexity to the interface in the library you can never ever take it out.
I thought in the js world it was normal to break compatibility whenever.

    declare function addClass(...classes: (string | string[] | (() => string))[]): void;
It's pretty straightforward in Typescript. And when you go to implement it, tsc will make sure you cover all the types your function claims to support.

> This is very useful to create a library like jquery that you can initialize straight away without needing (but also being able to use) the `new` keyword, just calling it like a function and always ensures it returns an instance.

Avoiding having to type "new" is not a very compelling reason to avoid Typescript, especially because Typescript won't let you make the mistake of calling the function without it. It's just not a problem.

That's an order of magnitude less clear in what the function expects than the examples I gave IMHO
Actually, it's not. With the type signature I understand what arguments the function can take. With your examples I have to _infer_ that, and there could be other restrictions that I wouldn't know. Like, can you pass a function and a string? Or do all the arguments have to be functions or not? The type signature tells me right away.
How are literal examples that are strings less clear than saying "string"? How do you know with just "string" the separator method, the format, etc? In my example you have type information AND string format information AND examples, while with TS you'd only have type information
Again, TS does not forbid you from having examples. Your docstrings don't replace Typescript, and Typescript doesn't replace good docstrings. But your docstrings are very unclear on which types you can mix together, and that has to be inferred. And if a user passes in something else by accident, it will fail at runtime rather than warning them the moment they write it.
Who's stopping you from giving examples in a docstring?
You can use recursion and template types to type some pretty complex string values now - I've seen cut down parsers for both SQL and TS written just via TS types which is madness but does show what can be done.

Whether the effort is worth it is, however, a totally different question to whether it's possible.

>To this day I haven't found a way of doing this in TS.

Just make a static method that does initialization if needed and returns a new instance?

https://www.typescripttutorial.net/typescript-tutorial/types...

I'm trying to not be too hard on people as I read this thread, but it's baffling to me that web devs are getting filtered by features that have been in other languages since the 80's and 90's.

There's "be flexible in what you accept", and then there's… "take a comma-separated string which you could parse into your arguments" :)
May be I want to pass JS string to be eval-ed. Flexible, huh.
Your problem would be solved pretty easily by making two changes to your API:

* Only accept an array of strings. Not a single string, not several strings, not several arrays of strings, and certainly not a space/comma-separated list of classes.

* Add another single overload where you accept a function that takes two parameters. That function can ignore its parameters if it wants to, and it returns a list of strings, so you don't need to accept several.

You have the same functionality, it's not harder to use for an end-user, and it's infinitely simpler to type.

If TypeScript steers authors away from either of these patterns then all the better in my opinion.
I don't design my APIs that way unless the language lets me write each (addClass) version as a separate function 'head'. I.e., Elixir and Haskell.

For the other 99% of programming languages, that kind of interface makes the addClass implementation too complicated and forces unneeded branching into it. Consider: when the app developer is calling addClass, they _know_ which interface variation they're using. So they can easy write e.g. addClasses instead of addClass ... completely removing the branching from the code altogether.

Your input and output types are much simpler and static analysis is much easier as well.

I don't agree that library developers should make user lives easier. I want from library to provide only `addClass('name1')`. I can write array iteration, it's not hard. I need library to have a stable interface, as simple as possible. And I need library to have quality implementation. I don't use libraries for fancy APIs. I use libraries for tested implementation code. If I need fancy API, I'll write it for my use-case which will be better for my application anyway.
A lot of times making the user's life easier is about having a single correct way to do something.
It's true. A type specification also has the role of documentation, quickly telling the user how to use the thing. A ridiculous typespec - to support an "easier" API - has the effect of making it impenetrable.
> To this day I haven't found a way of doing this in TS

You use an ambient declaration to declare the missing classish part of the type.

    declare function myLibrary(arg: Type): myLibrary;
    declare class myLibrary {
      member: Type;
      constructor(arg: Type);
    }

You see this pattern pretty often in DefinitelyTyped.
> being flexible in what you accept when possible

I disagree with this. When working with rxjs I wanted to emit a single string in an error handler. Since strings are iterable I ended up with the characters being emitted.

The library authors added this "flexibility" (implicit conversion from values to observables) which caused a subtle bug that took me a while to figure out. A type error (expected observable, got string) would have prevented this.

the type definition of the same is clearer imo and not only that it is enforced by both the ide and the compiler. Libraries typically DO NOT write a bunch of code examples of all the legitimate arguments that can be passed. Also in your example above,

> .addClass(function(node, i) {return 'name1'})

^ what is node? What is i? Seems intuitive to think it must be a dom reference and an index. But in different domains it's not always gonna be so clear. Like I am not familiar with umbrella js, but maybe node could be a jquery object and not a plain dom ref? With typescript you can just say

type GetClassName = (node: HTMLElement, i: index) => string | string[]

// see how I added string[]. So I don't have to add yet another example

// of a function returning a string array instead of a string

and then add it to the union of types that can be passed into addClass. Great, no more guessing based on the domain knowledge I have (or don't), it's crystal clear. And it forces the lib developer to have the discipline to make it crystal clear, which they usually don't I'm afraid.

I agree. I am the creator of the data table lib datagridxl.com and I like to make my methods as flexible as possible. Example:

grid.selectRows(2) // index grid.selectRows([3,5]) // range grid.selectRows([[1,2],[4,6]]) // multiple ranges

It fits in the JavaScript spirit of "we will make it work" which I love.

Other major thing that made me decide to develop in es6 instead of typescript was compilation times. After a ctrl+s it had to compile ts to js for 10 seconds, which is annoying for me, as i like to check & test every minor code change.

> It fits in the JavaScript spirit of "we will make it work" which I love.

Do you also use == and != for comparisons by default?

Check out the library, it's not that bad ;-)
> Other major thing that made me decide to develop in es6 instead of typescript was compilation times. After a ctrl+s it had to compile ts to js for 10 seconds, which is annoying for me, as i like to check & test every minor code change.

Typescript has a --watch mode that compiles as you work. Most test runners also often have a --watch mode. Test runners that support Typescript directly don't even need Typescript's --watch to be running as they'll do both, compile and test in a single step as you save. Anecdotally, the time it takes to run tests dwarfs any Typescript compile times and in a --watch mode of a test runner there's almost zero difference in the time it takes to watch ES2015+ tests or Typescript tests.