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!
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!):
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
> 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.
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
// 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 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.
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.
> That is the whole point in creating a library. To hide complexity in a nice easy to use interface/API.
I think that's a fair point, but generating types that satisfy all use cases is very challenging to get right -- disproportionately so. I could see a world where -- without proper tooling and growing complexity -- typescript libraries becomes so difficult to maintain that people give up or burn out. Maybe that's a pessimistic outlook but I already feel that way some days.
In strongly-typed programming languages, which includes Typescript, figuring out the types *of the interface* is not something that's done after the fact. `@types/*` is an exceptional project meant to back-port JS libraries to TypeScript, but that's the exception, not the rule.
If you write a library in TypeScript, determining what types are present as part of the interface is one of the very first thing that should be done.
If you're talking about type-inference, sure, I guess it's fine.
If you're talking about figuring out what types you're going to accept, you should absolutely be defining that on your own up-front. If you don't even know what your types are how is an end user going to figure it out?
Often libraries are bootstrapped from application code. Having a step function in complexity is not helpful in fostering an ecosystem with a broad range of maturity.
> That is the whole point in creating a library. To hide complexity in a nice easy to use interface/API.
Totally agree. Minor ergonomic changes in a library have a wildly disproportionate impact on developers. If I save a developer 5 seconds with a better type, and I have 1000 developers using my library, that's 5,000 seconds I just saved - and that's assuming they only ever use my type a single time! With this in mind, I'm totally OK paying a tax on making libraries a bit harder to write, if it means that the benefits of stronger types fan out. After all, writing that type could take 4,995 seconds for me to write and still be a net positive.
And honestly, 5 seconds isn't even close to accurate. Good type definitions have saved me hours.
> In effect, we are shifting complexity from end-developers to library developers
That is the whole reason to use libraries.
This blog post shows that types make development more enjoyable by shifting the complexity burden to library authors, who are vastly fewer in number than library users. It is a clear win.
Agreed. I updated the blog to add an example. Copy/pasting here:
I spend a decent amount of time in the redux world so `redux-toolkit` is a great
library to see how types are done *correctly* in a real codebase. To be clear,
they do a fantastic job with types, but the level of complexity is pretty
startling.
I suspect it’s a case of bolting on types to soupy JS but would also love to see some examples. I agree with the other commentators that part of the point of a library is to suck in complexity but if you’re regularly doing high level type kungfu there are probably bigger structural issues.
They probably just keep running into issues that seem really trivial but are unable to find info on fixing. It's the reason I put off writing things in typescript until relatively recently.
In that vein, if anyone here can tell me how the hell you functionally map over a typed object by key in typescript I'll be eternally grateful.
I always wanna do something like Object.keys(typedObjName).map(...) but that doesn't work.
It's so stupid and small and minor and it's never really prevented me from getting something done but it drives me out of my mind that I still can't find any clear documentation on how to do it.
Do you have an example for what you want to do, with an object before and after mapping? I don't understand what you mean by "functionally map over a typed object by key"
I think I have run into a similar issue. I wrote a lexer in Typescript. It is table-based, as is the parser that runs after it. The type for the table looks something like this:
I also have a type defined as 'type TokenID = keyof TokenTable<unknown>;' this makes it possible to check if a string is a valid key at compile-time. The innermost loop of the lexer is a for..in loop. This gives you the keys of the object. One problem: if you try to apply the TokenID type to the loop variable, you get this message: "The left-hand side of a 'for...in' statement cannot use a type annotation." Because of the design of JavaScript, TS cannot give object keys any other type but 'string', even though this type seems like a clear match.
To get the typechecking back on the keys, you either need to declare the loop variable outside of the loop itself, or use type casting like this:
let token = "";
let id: TokenID | undefined;
for (let key in patterns) {
const match = patterns[key as TokenID].exec(substring);
if (match && (match[0].length > token.length || key == "EOF")) {
token = match[0];
id = key as TokenID;
}
}
That's a possibility, yes. This is the only time in the program that a token table is iterated through, however. Most of the time a table is consulted to pursue an action in the parser. For example, there's a function table for when a statement is encountered, another for when an expression operand is encountered, etc. Each entry in the table is either an error message or code which completes the parsing of that statement. The awkwardness above is excusable when it is encountered so little. When writing expression-heavy stuff like 'Object.keys(typedObjName).map(...)' it's more of a problem.
https://jsfiddle.net/knvztxq7/1/
^ that code... is also working even though I swear it hasn't worked for me before so now I think I'm actually just losing my mind.
Yeah wow I can't reproduce my error now... I'll come back here if I figure out what I did.
possibly but the error (that I can't seem to get anymore) was always saying something about TypedObj missing an iterator, making Object.keys() unable to parse it. But I'm not getting that anymore so I have no idea what's going on. Like I said it's just a minor thing that's popped up a few times so I guess I'll just have to wait until it pops up again to know for sure what's up.
I disagree it's hard per se, but I'd agree it's hard to do well. Especially if you have a bunch of JS functions that return (or expect) objects with very similar but slightly different properties. Figuring out the overlap and the semantic relationship between the types is often challenging, because the original authors never thought about the problem in terms of what types of objects they were dealing with.
Writing new library and expecting people to actually use it without providing types is not realistic. You can write library in JS, but you will still need to provide types, since that's what ecosystem is right now.
I liked the article, and I'm sympathetic to the overall point...
> The kind of hoops I have to jump through to get types "just right" in a web app versus a library is dramatically different. It's rare in a web app for me to need constructs like conditional types, type operators, and overloads. As a library developer, they are heavily used. These constructs are highly dynamic and embed logic into your types. This leads into my next frustration with typescript which is debugging.
I would have like to see some examples of "conditional types, type operators, and overloads" in action, and an argument for why these constructs are so much more prevalent in library code than in application code.
I don't have a counter-argument, and I don't have any reason to doubt the author's insight, but after reading the article I don't feel like I have an increased understanding of the problem-space.
It's been ages since my last dive into the js galaxy, but from my outside perspective I read that as "the usual mess that we like to do to provide awesome backwards compatibility when introducing wildly changed API generations is very difficult to sneak past the typechecker". Not sure that I read it correctly, but it would certainly fit "conditional types, type operators, and overloads".
Why are they more prevalent in library code: to make changes non-breaking (or breaking, but fixable with minor tweaks). Of course types can also be a solution to this problem, by promoting consequences of incompatible change from runtime to compile time, but that only holds if you can assume that all calling code is type checked.
Because in application you're on consumer side, you have concrete types you're working with, in library on the other hand you're often providing generic code with parametric polymorphism, some constraints, you'll use type mapping etc.
Before judging it as "terrible" it would be good if author has more constructive criticism - if typescript is "terrible" what is not "terrible", haskell? What are specific things that can be changed to make it simpler? What if typescript is actually very good but the underlying type theory is just inherently complex?
You also have to appreciate what kind of value it brings to library users - and imho, in many cases, the value is so high that the whole discussion is a nobrainer.
In my opinion - if you want to use typescript, you need to learn it, especially when writing libraries. It doesn't make sense to want to use something and not want to learn it at the same time.
I can empathize with the author here that types can be very challenging to get right, especially with high amounts of dynamism.
However, I think that saying it is "terrible for library developers" is a bit far. I think its terrible for developers who want to make use of advance types... which ultimately doesn't depend if you're a library dev at all.
It boils down to: "Typescript learning curve gets really steep after the basics"
The author mentions they help maintain redux-saga. I don't want to dig on them, but from my personal experience, that library takes you down quite an opinionated application path with complexity inherit to it. Heck, just making types for redux was a pain, let alone adding async and additional composability.
My take is that the author has chosen complex tech to work with, and that influences the complexity in their types.
Something I've long wanted is a type-explainer - something that will let me highlight parts of a type definition and tell me in English what the fragment means. The example that OP linked elsewhere in this thread [0] is a good one - I know what parts of that mean, but I've never seen "T extends string = string" before.
And the purpose of "T extends string" is to carry the value of a string literal. For example, there's the string value "foo", but there's also a type "foo" which extends string and has only the one possible value.
The problem with the premise of the article is that it presupposes that a library’s interfaces will, and ultimately must, be complex. The great thing about TypeScript for library development, if you start with types, is that it strongly encourages you to create simpler and less flexible interfaces. That’s not to say you don’t sometimes need (or won’t sometimes choose) to use some of TypeScript’s more complex features. But a lot of the time you won’t, or should think seriously about whether your interfaces are becoming too complex.
Much of the complexity of TypeScript’s type system is a direct result of excessively dynamic interfaces in existing, untyped JavaScript. Starting with types is a good way to catch that early, rethink the interface, and either isolate it or avoid it entirely.
It was bad the first few years, with existing code being hard to type because it was creatively written. But now that almost everything is somewhat typed, JS has converged to a way of writing code, and you should consider the tradeoffs before choosing a different path than the current norm.
I really _wish_ this were true, but go look at a vareity of "modern" React projects. React is so flexible that I've seen nightmare implementations of what could be reduced to just a few short lines. Then people turn around and blame React as a 'garbage framework'. Then if you talk about a much more verbose and strict framework like Angular, people turn around and complain "I can't do what I want with this overly opinionated framework!"
Damned if you do, damned if you don't.
However, if you look behind the curtains of these infamous gripes (some of them so often repeated they are cliché), 99% of the time its people who just haven't spent enough time around a certain framework or language and go create a rant post (similar to OPs post)
You're free to love the tools and languages you use all the time. But it's bad practice to go around bad mouthing tools and frameworks you don't full understand or haven't used productively.
It also was missing a lot of crucial features early on, which made typing certain things impossible. Even now there's a few things that are a PITA to type, but as you say those edge cases can be largely avoided.
I made a similar comment in 2019 about "TS pushes you towards simpler APIs" [0], so I agree that it's both a good thing in principle, and something TS usage leans towards in practice.
At the same time... JS _is_ a very dynamic language, library design reflects that, and library code by its very nature must account for the different ways that users will want to call it. That causes library types to be much more complex than app types.
Especially since types are documentation. If you are using some meta dynamic type garbage, then I as a user of the API am left to wonder "Ok, this takes <U, K extends keyof U>... WTF is it expecting?"
This isn't to say that sort of thing is ALWAYS wrong, but rather it should be the exception and not the rule.
You do types because constraints make code clearer. By having a BF compiler in the middle of your template definition, you've defeated the types and you might as well put an `any` there with docs that say "here there be dragons!"
This is all well and good until you have to deal with other code and other APIs in your library (results from network requests, localstorage, untyped javascript libraries, etc.)
I'd rather not warp my whole project around a typing system if I don't need to. I also might not be the owner of every API I need to consume...
I think TS is great in certain cases- I love being able to get intellisense info from TS. When I download a new library and I get those helpful hints built right in from the libraries TS adoption, I love it. Typescript in this instance is reducing friction in my development workflow.
In my day job I work at an agency where we rapidly deliver a lot of new web properties. I have used Typescript on projects when it was a client requirement, but in our environment using Typescript generally feels like it overall adds friction and time to the project. Some team member inevitably gets stuck spending a bunch of extra time working with TS stuff: setting up the project with proper rules, writing some complex interface to deal with some random API we are either consuming or creating ourselves (in which case it is usually under rapid prototype development and needs to be updated constantly), etc.
I'm sure it is very helpful when working at scale with tons of developers on a highly stable and established project, but in my opinion it isn't generally applicable to every web property that it would be better on Typescript; it really depends on the scale of the project and the scale of the team maintaining it.
> I'd rather not warp my whole project around a typing system if I don't need to.
This sounds to me like MongoDB advocates like "psh! who needs a schema! don't slow me down, I'll just dump JSON into the database however I like." The types exist whether you write them down or not.
I agree with you that TS might not be great for everyone and every use case. But maybe you should just use `any` some more!
Every field in a MongoDB document (we encoded documents in BSON at the driver level) is typed and you can apply a schema to any collection if you choose to do so.
Python is also very dynamic, yet libraries rarely allow arguments to be number/string/list/dict/function at the same time.
Yet in JavaScript I see this all the time - pass a string URL, but we also accept a function which will return a string URL, or maybe an option object - { url: string, strict: boolean }
Right, but people compose. They take this union and put it in another object, which also allows multiple types for some field, and then they take that object and put it in another one, and then you have a 10 line type declaration for 3 fields.
I’m mostly sitting this out, but I’ll interject here because this is wrong. Union types are often used as an XOR (a | b as “either a or b, but not both”), but they’re an inclusive OR (Partial<a> & Partial<b>, as “some parts of a or b, the parts they share guaranteed”) which is almost never what anyone wants when dealing with objects. The pain is that you can’t distinguish them without a narrowing factor, and if you can’t the types are horridly complex.
I mean, that makes sense. If types `A` and `B` both have a function `doStuff`, is it bad to be able to do `x.doStuff()` without narrowing?
For example: every object’s prototype chain ends with `Object.prototype`, so all objects have a `toString` function. I shouldn’t be forced to narrow my `number|bigint|string` (for example) just to do `x.toString()`. If I was, we’d get this ugly and bloated mess:
function callToString(x: number | bigint | string)
{
if (typeof x === "number")
x.toString();
else if (typeof x === "bigint")
x.toString();
else
x.toString();
}
Regardless, they’re just copying JavaScript’s duck typing feature. It’s not much different than languages like Java and C# that have interfaces. In C#, if my parameter is IClonable, I don’t care what it is, but know there’s a `Clone()` method attached to it.
Python libraries kind of used to do this more, but seem to be doing it less now, in part because highly-polymorphic interfaces are a huge ugly pain in the butt with the Python type hinting system.
In general, if someone with experience in a domain is complaining that something is hard, it's safe to assume that the thing is actually at least kind of hard, and that they aren't just a crybaby about indentation and formatting.
Fair, it's not "hard". But I stand by my assertion that it's a huge ugly pain in the butt. That doesn't mean it's not worth doing, but that doesn't make it less huge, ugly, or painful.
I also assert that if NDFrame.loc were less polymorphic (e.g. if you had a separate accessor like NDFrame.mask for boolean subsetting) it would be easier to use, easier to reason about, easier to teach, easier to document, and easier to write type hints for.
The worst situation is when you want to have multiple return types depending on the argument values, like Pandas' inplace=True. Putting aside the problem of dependent types, even supporting only literal True and False requires you to write out every single overload twice, creating a literal combinatorial explosion of overloads.
And that's also ignoring the whole world of parametric polymorphism required for higher-order functions, factory functions etc.
Maybe the situation is no better in something like C++, but I don't know enough C++ to have an opinion on it.
To be fair, there are few things as here-be-dragons as the guts of Pandas. It'd be hard to find a more rarified example, but it served to further the conversation at least.
> Python libraries kind of used to do this more, but seem to be doing it less now…
I’d argue that it has to do with C extensions making it a lot of work to get it right.
Some library code does it (which is where I learned how to do it ‘correctly’) but in general, run of the mill code it is pretty rare. Just something like an overloaded __add__ turns into a page and a half of code and there’s a lot of functions in the numbers protocol struct.
I think allowing people to call functions in a vast variety of ways is a mistake. It's one of the things that frustrates me the most about the javascript ecosystem, it's hard to figure out the call signature for any given function because there's like 15 different ways to call it. I'd argue it's better to just have one well documented way. It's not even like there aren't patterns for it, ie passing an "options" object or something like that (although even then I think that can be an antipattern unless there truly needs to be a variety of options -- ie a db connection or something. Better just to have multiple functions with specific purposes instead of one mega function that does everything)
Agree, 100%. Make your function definition `foo(foo: Foo)` not `foo<T extends comparable | int | Bar>(foo: T)` Flexibility is the enemy of a type system and library design in general.
I don't think allowing users to call libraries in all manner of crazy ways is a good thing at all, and I don't think anyone would really be that put out by following a more pythonic approach of just allowing one way to do something.
Yeah but you're writing libraries in Javascript, that ship has sailed already. You're gonna just need a bit fat switch statement to check your input types on every public function you ever write.
It's never too late to stop making bad designs. I've written plenty of (internal) libraries and if someone asked me to put in a big fat switch statement I'd ask them why they need it.
Case in point, over the weekend I tried creating an npm package for a custom hook. Since it's public-facing, I wanted to use TS so I could expose types for users.
Part of this hook is that users can pass in custom data. I tried to create a type that was basically "an object of any string key, with the type value". Gave up after like an hour of banging my head against my desk. Maybe I'm new to TS, but FFS it should not be that hard to do.
I agree, and it’s the same with Python. If you start with static, type-safe first, then Python’s flexibility ends up being so horribly convoluted that you avoid it in favor of simpler constructs. **kwargs? Massive union types? Super flexible dicts? Rather not!
I’ll define 50 strongly typed constructs over having to run my code through 50 times to catch all the runtime bugs.
It accepts, as a parameter, a URL for Apple's REST API. My library handles authentication, and returns the parsed JSON result, with a handful of tweaks to make the API more usable in JavaScript.
Depending on which URL you request, you'll get different result object back. You could get a single object in response, or an array of objects, and the type of returned objects is different for each URL type.
How would you add TypeScript types to this API? Well, Apple provides an OpenAPI documentation of all of their URLs, which I could use to autogenerate types, but then, how would I handle all of those types in response to the user's string input?
can parse its parameters into a Request type with shopid, itemid, and args[] array parameters. This would catch a bug if you had a typo, e.g. "itmid".
But the code to do that looks like this:
type IsParameter<Part> = Part extends `[${infer ParamName}]` ? ParamName : never;
type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}`
? IsParameter<PartA> | FilteredParts<PartB>
: IsParameter<Path>;
type ParamValue<Key> = Key extends `...${infer Anything}` ? string[] : number;
type RemovePrefixDots<Key> = Key extends `...${infer Name}` ? Name : Key;
type Params<Path> = {
[Key in FilteredParts<Path> as RemovePrefixDots<Key>]: ParamValue<Key>;
};
type CallbackFn<Path> = (req: { params: Params<Path> }) => void;
function get<Path extends string>(path: Path, callback: CallbackFn<Path>) {
// TODO: implement
}
Nifty, eh? But, as the article says: how would you test this code? How would you debug it?
Clearly, I wouldn't do that. Instead, I'd write a script to autogenerate individual methods, e.g. instead of get(`apps/${appId}`) that returns a parsed JSON blob, I'd autogenerate a getApp(appId) method that returns an App object.
But that API isn't any simpler than the API I already have; it's just different.
And let's not forget that I'd have to write a script to autogenerate these methods (or just their types) based on Apple's OpenAPI specification, and now I have to maintain that code, updating my @types/node-app-store-connect-api definition every time Apple introduces a new URL you can request. Testing and debugging that is a challenge in its own right.
And even if I did it, the complexity of my library just went from a few hundred lines of glue code to 1,000+ lines of type generation (plus tests for the generated types).
This is in no way worth it for me. As a library developer, adding TypeScript types would make my life harder.
> And even if I did it, the complexity of my library just went from a few hundred lines of glue code to 1,000+ lines of type generation (plus tests for the generated types).
I would say that the increased complexity of your project would accurately reflect the complexity of the underlying API
> As a library developer, adding TypeScript types would make my life harder.
Yes, but it would mean that the various parameters passed to your library would now be type safe, which sounds worth the work
One clear moral of this story is that, as a library developer, I need a lot more documentation and tooling support from the TypeScript team.
That string-parsing thing isn't in TypeScript's documentation at all; it's a random blog post. I'm lucky I found it. TypeScript's site should document how to do this.
Furthermore, it should be easier to debug parsed stringly-typed APIs using the `infer` keyword. Today, I just run the compiler over and over again until it stops throwing errors, and the errors are really not very helpful.
There should be a standard tool to help me test that I used `infer` correctly, ideally helping me by fuzzing the code to see if I allowed something I shouldn't have allowed, or blocked something that I should have allowed, and to test against various versions of TypeScript.
Narrowing types shouldn't be done just by nesting ternary expressions. As the article notes, "It's pretty common in style guides to never nest ternaries. In typescript, that's the only way to narrow types based on other types. It's a mess!"
Lastly, this idea doesn't sit right with me:
> I would say that the increased complexity of your project would accurately reflect the complexity of the underlying API
I designed my library so it doesn't need to incorporate that complexity. Users of my API have to learn the API, but they can learn it from Apple, by reading Apple's documentation, and by inspecting the objects that Apple actually returns (rather than what their OpenAPI specification says they'll return).
Incorporating that complexity into my library will make my library harder to work with, more difficult for others to contribute to my library. As the article says, "Types make it much harder to maintain a js library, and especially difficult to contribute to them."
TypeScript is forcing me to duplicate the API's complexity in my code. Even if you think that "sounds worth the work," I think you have to agree that the work load is much higher than the work I've already done.
> Furthermore, it should be easier to debug parsed stringly-typed APIs using the `infer` keyword. Today, I just run the compiler over and over again until it stops throwing errors, and the errors are really not very helpful.
This paragraph kind of sums up both the OP and this whole HN discussion of it. So I'll throw my comment on here, although I suppose it could go pretty much anywhere.
Anyway, one way to read it all is: This just goes to show that
1) JavaScript sucks;
2) Therefore the whole world of "Web apps" built on it is an unmaintainable buggy nightmare;
3) Trying to graft "strong" (not really) typing on top of it in the shape of TypeScript is just polishing a turd.
It's like somewhere around 1995/2000 the world of computing jumped through a time warp back to 1975, and we've been stuck there, in a world dominated by the new BASIC, ever since.
> Depending on which URL you request, you'll get different result object back.
This sounds awful to me. Why wouldn't it be separate functions? I absolutely would not want that at all as an end user, I'd find it confusing and frustrating.
Because in order to have separate functions, I'd have to define separate functions for every function that Apple supports, i.e. I'd have to autogenerate the list from Apple's OpenAPI specification.
Furthermore, if Apple introduces a new function, I'd have to release a new version of my library to support it. Today, my library can handle any object Apple's API supports, without upgrading.
Swagger can generate that sort of thing. I guess I don't know what things your library provides so I don't want to bash it, but having a generic read/get that doesn't provide types doesn't seem like a big improvement over just using a general purpose tool like axios or fetch.
The App Store Connect API requires scoped, time-limited tokens to be generated and has specific filtering, pagination, and rate-limiting mechanisms built into every resource. I haven’t looked at this particular library, but I would expect that it’s quite useful beyond simply making the HTTP requests.
I think it’s great that you publish and maintain this library and nobody can or should tell you to put more work into it. That said a package that did have explicit methods for every endpoint with documentation of what it does and what the parameters are would be awesome for me as a user. The docs will pop up when I write the code and make it easy to explore the different methods from my editor. No need to switch to the apple docs all the time (but probably still sometimes). And again I still think your library can stay as it is. I really don’t think this string parsing magic is the way to go. It can be useful in some simple cases, but specifying a whole API goes to far imho.
Agree 100%. Redux et al are terrible examples where complexity is through the roof, these libraries were written way before TypeScript was the norm. You can eliminate almost all of that by having simpler interfaces, but that requires a different API built with types in mind.
We were trying to add types to some of our code. And the types were getting ridiculously convoluted. And at that point it was like - maybe it's not the types that are the problem, maybe it's the interface that needs to be rewritten.
The OP's example of createActions smells like such a scenario. The way redux mashes up payloads and discriminators makes it hard to type correctly - as in objects are not simply composed, they're extended by adding properties, and for no immediately obvious reason (which might be because of my inexperience, granted).
Had redux been developed in typescript from the start, I doubt it would have chosen this API. Then again, the current version is already in typescript, so perhaps that optimism is unwarranted; unfortunately my experience with it is years ago already.
This particular use case is trying to strongly type the "Flux Standard Action" object shape. An object _may_ have a `payload` field for its data, _may_ have an `error` field that indicates this action represents an error, and _may_ have a `meta` field that provides additional descriptive information.
This isn't part of the Redux core package, but it's a convention the community adopted shortly after Redux became popular. Redux Toolkit uses that as the standard structure for an action object. `createAction` defaults to just `{type, payload}`, but you can optionally provide a callback that adds the other `meta` and `error` fields. So yes, there's some additional complexity here because of the optional field contents, and this type is telling TS what the final type should look like based on which fields exist.
Note that as an end user, you normally don't even call `createAction` yourself - it's automatically called as part of our `createSlice` API. What end users normally write is:
export const counterSlice = createSlice({
name: 'counter',
// `createSlice` will infer the state type from the `initialState` argument
initialState,
reducers: {
increment: state => {
state.value += 1
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
// `action.payload` is now a `number`
state.value += action.payload
}
}
})
Yeah, Redux Saga is known for heavily using JS generators and I wonder how much of this is just the relative obscurity of that language feature for him. It's not exactly a straightforward library.
How do library developers debug their highly dynamic and heavy use of conditional types, overloads?
Don’t do that. It dramatically increases compile time and results in a maintenance nightmare. Keep types primitive.
In my own code I use type unions of primitives and/or named types. For objects I use interfaces. With that I am able to provides types for about 98% of my code. That left over 2% is generally for extending the global Array type or extending an event.
I wrote a set of React components that gets 46,000 npm downloads a month and typescript is a godsend. My library is a bridge between two heavily popular projects, so my dependency tree is fairly intertwined. The library solves a real user problem and does it efficiently. It isn't totally perfect, but it covers 98% of the use cases.
I wrote comprehensive tests as I developed everything. I have good documentation. I have an example app people can play with. I have codesandboxes. Yes, it was a lot of work, but that's what building products is all about.
I've maintained this library for over 3 years now. I've upgraded it many times as the underlying dependencies have changed. A few times with backwards incompatible changes. I get outside contributors doing great work.
I do releases on a regular basis, without worry, because my test suite is that good and because I know that if the underlying types change, tsc will catch that too. These things have saved me countless hours of work finding bugs and fixing issues.
In other words, I call bullshit on this entire blog post. Ignore it. Typescript is the correct solution. What is the alternative? Just using JS and letting things break without knowing it? Come on.
Learn how to use types. Learn how to use a professional IDE, like IDEA, that shows you what's wrong as you develop. Learn how to write tests. Learn how to use a debugger. These are all things any decent developer should know and practice on a regular basis.
It's the first of his 5 gripes. There's only 5 of them and they're titled. A few of the other gripes also mention the lack of documentation, so it definitely appears to be his main gripe.
I agree with GP, you don't seem to have read his article and knee jerked a reply.
And all you have to do to prove your point is to link some great documentation explaining how to make complex types for libraries and how to test them.
What I said is that I call bullshit on his 5 points. None of them are valid and they are just opinions of a developer who hasn't put the effort in and isn't providing concrete examples of what is actually wrong. I actually find it difficult to understand why this is even on the front page of HN, maybe it is a slow news day?
I feel that my point is proven with a successful multiple year open source Typescript library.
Great documentation? That's subjective. I'd start with a deep reading of the official docs as they are quite good: https://www.typescriptlang.org/docs/
> I feel that my point is proven with a successful multiple year open source Typescript library.
That only proves that it's possible to do, not that the language facilitates it or that certain modifications to the language/ecosystem wouldn't greatly improve the process. The point of the article is the latter, not the former.
True, I paraphrased what I said so that matt could hopefully understand a bit better that I did indeed read the blog post. I want my 10 minutes back please.
Good for you. I was somewhat disappointed with TypeScript in the beginning, because I tried to use the type system like I would use it in Swift or Scala. That's not really a good idea. If instead, you view TypeScript as a smoother implementation of JavaScript + JSDoc, then it becomes a really powerful tool, which I like more and more!
I'm agreeing with you here. If you use plain JS and change the underlying types that are being passed around, but things are still working, you're basically getting lucky.
If you write comprehensive unit tests for your javascript, that should effectively also work too... this is what ruby developers pretty much had to do.
We can see that over time based on the decline of ruby popularity in general, that fell out of style though. It is too easy to make a mistake and everyone was just duplicating what a compiler does for us.
I can't tell you how many hours I would sit there pair programming with other ruby developers just trying to figure out what 'type' an object is. Mind boggling, but it was good to get those high paid consulting hours.
We were using that and it is indeed excellent (along with most of JetBrains IDE products). It starts to fall down on all the meta programming we were dealing with. With a dynamic meta programmed DSL, there is only so much RM can deal with.
Not directly. I've been developing open source for almost 30 years now (I co-founded Apache Java/Jakarta). I consider that the experience gained from developing extra curricular projects pays for itself in other ways. For example, I've never had to interview or even look for a job and I've never been laid off.
I really wish I could work in OSS more. Have contributed to several libraries intermittently. How do you find time to do both OSS work and your day job? does your employer allow your OSS contributions to count as work there? I wish I could find the key to that balance.
Whenever I find those guys on Github with several maintained multi-thousand star projects, I wonder "how do they do it?" (fund themselves + work on OSS). I am always curious about the underlying life stuff
The way that I look at it is that the vast majority of developers depend on some open source project. This means that we find bugs or missing features in other peoples code in our day to day coding. That's a perfect excuse to contribute upstream. If your company has legal policies against that, then honestly... there are other jobs out there. I know that sounds kind of blunt, but I've always made it clear to my employers before I start, that I'm going to contribute to the greater good as part of my work.
In this case, I needed the code for an internal project and decided that the company would benefit the most from making it a library and getting outside help on it. It was an itch that I knew many other people needed scratched because there was a competing project that had gone silent and was incomplete/poor quality. That justified my time spent... I had to write the code either way, might as well make it open.
When I look for people to hire, and I've done a lot of hiring over the years, I primarily look for people like myself who put that extra effort in. Either in their own personal projects or by at least contributing to something upstream. Even just simple documentation changes is something I'd hire over. I fully understand that people have lives outside of work, but honestly... if you're going to be a top developer, you've got to put in a top level of effort, because other people around you are.
> Typescript is the correct solution. What is the alternative? Just using JS and letting things break without knowing it? Come on.
Surely that's not the only alternative.
Here's a few off the top of my head:
- Being able to write complex types in a syntax that more closely resembles JavaScript. Using Array length for counting or nested ternaries for if logic gets old fast.
> Being able to write complex types in a syntax that more closely resembles JavaScript
Yikes. This is how you end up in preprocessor hell. Macros are generally not a good thing. (Fyi: TS types are already Turing-complete which is arguably a mistake.)
> Being able to debug types, not console output
Eh. It's not like Java has a "type debugger." Why is this needed? Why are your types so complex? Weird ask.
> More comprehensive documentation on writing advanced types
Really beating the same drum here.
> A typescript specification
What does this mean? We have a pretty clear typescript spec[1].
> Yikes. This is how you end up in preprocessor hell. Macros are generally not a good thing.
Not suggesting macros, rather an alternative way to define types.
A function that accepts and returns types. Seeing as typescript has a JavaScript interpreter I thought it might be feasible but I'm just spitballing it.
> Eh. It's not like Java has a "type debugger." Why is this needed? Why are your types so complex? Weird ask.
I don't buy this argument.
Writing any type of complex types with recursion, arrays or ternaries sucks and it will take more than "this is how Java does it" to convince me otherwise. I like to debug with a debugger, not my head.
Take a look at the examples in the article to see some complex types and read some tweets from library authors complaining on twitter. I can find some examples if you're interested.
> We have a pretty clear typescript spec
I hadn't actually seen this and it looks interesting but seems pretty out of date, Typescript 1.8?
The article (and ensuing discussion) are about _library_ code, which necessarily has types that are orders of magnitude more complex than application code (source: see some of the links in the article).
The reason this isn't a problem in Java, C# etc is that the things these libraries do simply _aren't possible_ in the Java type system, so no one does them, and users end up with (usually) clunkier libraries.
React has its roots in StandardML and it's showing them here. Java, C# (and ma y other languages) barely scrape the surface of what you can do with a type system.
Now there's definitely a question of when you start hitting diminishing returns but you can definitely do so much with and extract confidence from very strongly typed languages.
> The article (and ensuing discussion) are about _library_ code, which necessarily has types that are orders of magnitude more complex than application code (source: see some of the links in the article).
Are you somehow implying that C++ or Java don't have libraries? As a sidenote, React (and that entire ecosystem) is an architectural hellscape (see Vue for much more streamlined & cleaner code).
No, I'm just saying the libraries in those languages tend to be designed differently, to accommodate the more limited type systems (but I don't use C++, so I'm sure templates can do a lot of things I'm not aware of).
I'm also not necessarily saying this is a good or bad thing overall, since it can so easily lead to extra complexity.
FWIW, I'd choose C#/Rust/Kotlin/etc over TS 100% of the time for projects that don't need to run in a browser.
> In other words, I call bullshit on this entire blog post. Ignore it. Typescript is the correct solution. What is the alternative? Just using JS and letting things break without knowing it?
It seems like you and I read a different article. The post I read did not advocate for going back to JS; rather it advocated for better tooling and documentation (with examples, ideally) for library developers
Conclusion
I love typescript and think the team working on it are incredible. Typescript has completely changed the FE landscape and wouldn't want to dismiss its contributions.
But as a library developer, we need:
- better documentation,
- better tooling, and
- to spend less time making tsc happy.
I shouldn't have to read the typescript compiler source code in order to figure out why it's resolving a piece of my code to a specific type.
I developed a library used by the React app I work on to cache network results in localstorage (to reduce the number of expensive requests made on page refreshes) and generate React-Query query-functions to read the result from localStorage first before fetching from the network as a fallback (if the data is stale or uncached).
It's not an especially large library, but I felt all of the pain points the author describes. It's great that you didn't with your library (and I'm sure there are large classes of libraries whose authors wouldn't feel these pain points), but it's a very real issue if your dealing heavily with generics, serialization/deserialization, and/or complex type interactions
The author of react-query seems to be very clear at communicating how the library works and the library itself provides a great developer experience, but it's also an example of how much work can go into correct typing in library code.
Honestly, if that's what it takes to make something work with Typescript I'm surprised anyone uses it. I thought Scala/Cats was the only language/cult that indulged typism to this extent. **d save us from the static typing Spanish Inquisition and bring back dynamic languages.
Seeing one the worst implementation of static typing and then blaming static typing as a whole is unfair. gradual typing is just an abomination in practice as can be seen in both TS and Python. Maybe a language that's build from the start with gradual typing in mind can make it good but I doubt it. Static and dynamic typing are just at odds in general.
The types in Typescript are amazing. I believe they’re turing complete? The problem is that this allows people to do many really complicated things that’d just be avoided or impossible in other languages.
Yeah, this is the sentiment I keep feeling looking at most of the examples being provided in this thread and the article. A lot of the things people are doing here aren't even possible in most static typed object oriented languages or are hard to do and generally discouraged, and violate things like the Liskov Substitution Principle and Open/Closed Principle (especially the Open/Closed Principle; many of these examples are redux adjacent and a lot of them require modifying existing State object instances rather than extending them at a class level per the principle).
Many of these things are easy to do in JS because JS wasn't built to be static typed object oriented language. Many of these are hard to type because the underlying language is so permissive.
As library authors the desire is to be as permissive as possible, to use the "simplicity" of the untyped JS language to express an API surface that accepts any combination of possible inputs and does the greatest amount of work with that. There are many "JS native" libraries that do that, including JQuery's $ "operator" that was a swiss army knife of a million different tasks all using a single constructor which created object instances that any number of plugins mutated over time. As a JQuery user that was a very easy experience to work with, it's permissivity felt like simplicity and easy-to-learn. As someone who briefly spent time debugging JQuery types definitions in Typescript, that was an incredible nightmare. (That was also many, many versions of Typescript back with fewer typing tools, many of which would have helped a lot, but also made everything even more complex than it was at the time.)
I do feel like a lot of library authors sometimes need to ask themselves as types grow more complicated in their libraries if the trade-offs are worth it. That "simple" API they are trying to give their users, is there a more "complicated" API with simpler types to use instead? Sometimes that's actually the simpler API to use understand too, but many of your end users see your types indirectly and simpler types in the API are simpler experiences of those types.
The article mentions needing to add lots of overloads and that's specifically something I'm thinking about there. Another example in this thread included a function that could take a set of arguments as either an object ({ a?: thing, b?: someOtherThing, c?: thirdThing }) or tuple ([a, b, c]: [thing, someOtherThing?, thirdThing?]) and in both cases many of the parts were optional, complicated further by the thing type being generic itself and the tuple accepting different orders of parameters. Can you just pick one, object or tuple for your API? Users have "less freedom" to do as they wish and the result seems less "simple", but you can eliminate so much complexity in your types. If you do still need both ways, maybe it's reasonable to split from "overloads" to different functions setThingsObject(things: { a?: thing, b?: someOtherThing, c?: thirdThing }) versus setThingsQuickly(things: [thing?, someOtherThing?, thirdThing? ]).
It's easy to armchair quarterback refactor other people's APIs, of course, but it's certainly a factor when I'm building my own APIs: These types are starting to get complicated here, should I refactor to simpler types? Should this API be split into two different/distinct endpoints to simplify the type signature? And so forth.
Some complexity is unavoidable, of course, but sometimes it is worth trading "simple, permissive JS with incredibly complex types" for "tedious explosion of JS that looks complex at first glance with incredibly simple types". It's a deep trade-off space to explore and what's right for any individual project is deeply personal opinion.
That is terrible. Good luck onboarding new developers or junior team members into your project.
I get playing with technology for side projects, but for something commercial there is no way I would sign off on including that package into a project.
This is what types look like when you're on the library side. The post made very clear they aren't talking about the app dev side. The library types are complex like this so that the app devs have a smoother experience.
I really wish Typescript had a great pattern matching system. It would be very nice indeed for structural typing, everything would be easier to read, probably some ways to add more power/expressivity, etc.
eg
type GetOptions<T> =
// Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
T extends {
queryFnData: infer TQueryFnData
error?: infer TError
data: infer TData
}
? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
? UseQueryOptionsForUseQueries<TQueryFnData, TError>
: T extends { data: infer TData; error?: infer TError }
? UseQueryOptionsForUseQueries<unknown, TError, TData>
: // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
T extends [infer TQueryFnData, infer TError, infer TData]
// ...
becomes
type GetOptions<T> =
// Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
T extends match {
{ queryFnData: infer TQueryFnData, error?: infer TError, data: infer TData } => UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
{ queryFnData: infer TQueryFnData; error?: infer TError } => UseQueryOptionsForUseQueries<TQueryFnData, TError>,
{ data: infer TData; error?: infer TError } => UseQueryOptionsForUseQueries<unknown, TError, TData>,
[infer TQueryFnData, infer TError, infer TData] => UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
[infer TQueryFnData, infer TError] => UseQueryOptionsForUseQueries<TQueryFnData, TError>
// ...
}
or even, to make the top level way easy to understand in this case:
type GetOptions<T> =
T extends match {
Part1 => Part1Match
Part2 => Part2Match
Part3 => Part3Match
}
where Part1Match etc are themselves pattern matches, so you can compose them like functions.
IME it's precisely the expressivity of nested types and generics and variations thereof that makes it hard for the library author.
The expressivity makes it possible for library authors have IntelliSense give more useful suggestions to the consumer, make automatic inference work in more places for the consumer, and make more illegal usage be impossible, at the expense of complicated type constructs. But of course all these is somewhat optional.
You answered your own question, learn how typing and generics work and you wouldn’t have had those issues.
The entire article persecuted TS because the author wanted to learn quicker with little effort. Writing TypeScript in a node, app or library makes no difference. It is a language, NOT a framework.
Yes, and a great way to do that is to read documentation, of which Typescript is frequently lacking. For example, I got a suggestion in the discord recently to use "generic parameter defaults" for a problem I was having, which were documented... in the release notes for Typescript 2.3... and no where else: https://www.typescriptlang.org/docs/handbook/release-notes/t...
> Yes, and a great way to do that is to read documentation, of which Typescript is frequently lacking.
I completely disagree, and I was surprised by this sort of comment.
Find me a single programming language whose docs are as good as TypeScript's docs and reference. I'd be surprised if you could come up with a single example.
C#, C++, Java (actually I am betting no one can beat that one)
I think you dismissed the actual issue he faced- gen type defaults, which I just looked is still not actually documented, it's just in the release notes.
Don't get me wrong, TS is my favorite language, but let's not gloss over things.
Recently learning rust (spare time) and c++ for work. The documentation for these 2 are awesome!
PostgreSQL has great docs too
I think we are better at writing docs for programming languages than we are at libraries and frameworks. Maybe because languages are more ‘fixed’ and less likely to change so over time they just get better and better.
You picked two of the best documented projects around in Rust and PostgreSQL. I don't think that inferior documentation for libraries is a foregone conclusion. Rust's serde library is exceptionally well documented IMO.
It's a matter of having the skill and the will though, and I feel like a lot of projects simply don't emphasize documentation. Even so, not everyone's going to be a good technical writer.
What I mean is I think in general languages tend to have better docs.
Vue has excellent docs imo.
But in terms of technical writing being a skill. I agree. I appreciate the effort people put into docs. I try my best but I’m not very good. So I have a lot of respect for anyone who can sit and write excellent docs.
It's been a long time since I've dug for C++ docs but in general I found Qt to be well documented (if a bit disorganized with the newer stuff). STL documentation always seemed inaccessible to me, but then again I'm working off of some pretty hazy memories.
Commercially supported software Qt (and in the parent post, Vue) tend to have good docs. It makes sense: supporting developers is their business model. Likewise tools with ecosystems of companies (postgresql) tend to get good docs over time. The bigger pain points are tools supported by a major company ... but that used by the business rather than sold. There probably are _some_ good docs - especially at the project launch or major milestones - but they aren't as actively curated.
I think others have covered it here, but I want to point out that I actually think Typescript's docs are quite good for end-users! But, like the article here points out, lacking, for library authors.
There are lots of ways to do things that you kind of just have to learn from reading blog posts, or reading code and then asking in the discord when you see something undocumented (because have you tried googling "what does <T=...> in Typescript mean"?).
Ideally, at the very least, all syntactic features and keywords of the language should be documented, but it's more than that; Typescript is a metalanguage, and authors of many libraries have also developed patterns which are essential for describing complex types. Some of these patterns are documented (for example, discriminating union), and some are not (for example, opaque types). And some features which are documented could do with a lot more exposition and functional examples ("as const"), or notes on when to avoid (enum).
Whilst that's good, the TS documents should document this. Google searching and stackoverflow is not a valid replacement for actual documentation, IMO.
Certainly they should document it, but as an answer to "(because have you tried googling "what does <T=...> in Typescript mean"?)", I think it seems pretty conclusive :P
Golang entire standard library is documented. Plus with how the language is designed docs don’t need to change with new versions. Just with new functions.
More importantly, golang has https://go.dev/ref/spec — most of the time I wished for better TypeScript documentation it's about syntax (though I did need to go read the default bundled libraries a couple times for details, that's rarer).
Is the argument "Typescript should be improved" or "We should allocate enough time/money from other things to improve Typescript"?
The former is vacuously true once you know it is an option. Everything would be better if it could be improved.
The latter is subject to questions about cost. If Typescript had best in its class docs, that would imply that other communities had failed to find or execute a practical option to turn time/money into docs improvements. That would be moderately persuasive evidence that Typescript's resources are better spent elsewhere.
---------
On an object level: Django and the python community tend to have great docs.
PHP used to have amazing documentation back in the days.
Everything spelled out. No "you must read the whole thing or have worked with it for years to understand page 5".
Plenty of inline examples and even more user submitted ones (although as with all user generated content, some caution should be used when reading those).
Have we used the same PHP? IME it's usually necessary to read the comments on the documentation for important information, which is frequently totally absent in the actual documentation.
I love Typescript but I just don’t think this is correct. Look at the Node documentation and then the Typescript documentation. The former tries to describe every behavior, the latter tries to give you a 101 in the feature. This is especially clear when it comes to the Deno and SWC teams’ attempts to reimplement tsc in a faster language - typescript doesn’t have a spec so they’re stuck looking at the tsc codebase to infer behavior.
It's not open-source or really polished for consumption. I did get permission to open-source it, but unfortunately didn't have the time to improve it to the point where it would be broadly useful.
> The post's author simply lacks the education and experience to actually have a valid opinion
Besides your take being just outrageously exclusionary – the author is Eric Bower, a principal engineer who's also a Redux Saga maintainer. They're more than qualified to weigh in on this.
And their opinion is to push the complexities further up the ladder onto the makers of the tooling.
I’ve seen some crazy assed stuff they do with C++ templates and my reaction isn’t that they need to be spending their time on writing blog posts to explain the dark mysteries to me but that maybe I’m not the one who should be trying to write a library without spending a massive amount of time learning how this stuff works.
Sure, I can beat a template into submission using the famously helpful error messages the compiler provides and some random blog posts by people who actually understand what’s going on and I’m very thankful that they spent the time explaining it. Even having 1000 line long error messages helps once you learn what it is really complaining about. And this isn’t even my literal job, just something I do because I enjoy it.
This is yet another comment indicating the author lacks enough knowledge to even talk about this. Why is this the default response from HN on matters like these?
Might have something to how the author chose to express his opinions. FWIW, I got the same impression from the article, but did not bother to write a comment until now.
IMO, the author could have done better by providing more concrete examples for the points of criticism, and understating the conclusion or moving it to the end or dropping it entirely, as I think it's better to give the reader the choice to make up his own mind, given the facts and evidence.
Just because I disagree with their premise (tool writers need to take responsibility for my lack of knowledge) doesn’t mean I think they can’t talk about it.
I mean, if they weren’t talking about it I’d have nothing to disagree with and have to do something productive.
And, you know, appeal to authority isn’t a valid counter argument…
You've both missed the point of the article. The author isn't saying TS (or typed languages in general) are bad, just that the tooling and documentation for _library authors_ could be better.
The things the author talks about aren't even remotely possible in the type systems of pretty much any other mainstream language, so I don't know how you could interpret this as a "TS bad" post if you read past the headline.
Anyone can write bad software with terrible APIs. That isn't the problem of the language and certainly doesn't make it 'terrible for library developers'.
My point is that I've written a library that gets a lot of use. It has decent enough APIs. Nobody is writing blog posts like this about my stuff.
How? None of what you are talking about directly relates to the article. It feels like you are just reacting to the title and not the contents.
For instance, you say:
> I call bullshit on this entire blog post. Ignore it. Typescript is the correct solution. What is the alternative? Just using JS and letting things break without knowing it? Come on.
But the article says:
> I love typescript and think the team working on it are incredible. Typescript has completely changed the FE landscape and wouldn't want to dismiss its contributions.
> But as a library developer, we need:
> - better documentation,
> - better tooling, and
> - to spend less time making tsc happy.
This is not an article advocating for “just using JS and letting things break”, so why are you reacting as if it is?
> This is not an article advocating for “just using JS and letting things break”, so why are you reacting as if it is?
Because the tone of the article seems to be that Typescript is horrible, and the tenor seems to be that the author is blaming his own incompetence on the language.
He has a point hidden in there somewhere, but I needed the HN comments to point it out to me.
- Documentation: there's docs on how to use the language as a whole, but nothing focused on writing libraries (how to build/publish, type tricks that libraries may need that apps don't, etc)
- Debugging: debugging types is a nightmare - you basically have to hover over temp variables to see what intermediate values are
- Complexity: TS types for real-world lib code can get complicated
- Testing: testing types is necessary, and the community has had to invent tools to help with that
- Maintenance: maintaining types and responding to issues takes up time
None of those are comments that "TS is horrible". Those are all straightforward concerns with the experience of writing and maintaining a TS-typed library, and as a Redux maintainer I can back up every single one of those. I actually did a talk on "Lessons from Maintaining TS Libraries" a few months ago, and I hit all of those points myself:
> Documentation: there's docs on how to use the language as a whole, but nothing focused on writing libraries (how to build/publish, type tricks that libraries may need that apps don't, etc)
Why would there be? Building and publishing libraries is a npm thing, and has nothing whatsoever to do with Typescript. Type tricks that libraries may need? If you are going to write a section like that it’s pointless to write it about libraries in specific. There is nothing about building libraries in specific because Typescript isn’t about building libraries.
> Debugging: debugging types is a nightmare - you basically have to hover over temp variables to see what intermediate values are
Sort of, if you’ve managed to make your types complex enough that you cannot reason about them. But then again, I don’t know of any other language that feels the need to provide a type debugger. The fact that people working with Typescript think they need it is either testament to their competence, or the ridiculous constructions they make.
> Complexity: TS types for real-world lib code can get complicated
Well, yeah. But it’s exactly as complicated as you make it.
> Testing: testing types is necessary, and the community has had to invent tools to help with that
I don’t know of any other language that thinks testing types is necessary. The big issue in Typescript is that types don’t guarantee anything about runtime behavior.
> Maintenance: maintaining types and responding to issues takes up time
Well yes. Maintaining any library in any language takes time. Do people think this is unique to Typescript?
I guess my own point mainly comes down to no other language having any of these things, and the OP’s idea that it’s on the TS authors to provide it feels misplaced. It’s not core to the language, and a lot of the problems are self inflicted.
I'm afraid I'll have to disagree with all of the above, especially the part about building libs.
Per the OP's article and my slides, there _are_ many aspects of maintaining a lib that are unique to TS above and beyond a plain JS lib, and it would be really helpful to have additional docs and tools available to help with our use cases.
The only convincing reason to use TS I've seen so far as a library author, which TBF is actually not about TS but about TSDoc/JSDoc, is how the autocomplete in VS Code works. Going from an empty/barebones autocomplete to actually having the documentation in-line is a pretty nice extra feature for my users IMHO:
Heh, it's amusing to see Redux Toolkit referenced here. I'm one of the two main RTK maintainers. My co-maintainer Lenz Weber is responsible for most of our TS type wizardry.
Agreed that writing TS types for libs can be a pain. I actually did a talk recently on "Lessons Learned Maintaining TS Libraries" [0], where I talked about some of the techniques we used, and some possible TS changes that would be helpful for us as maintainers.
As one recent example, TS made a change in a 4.8 pre-alpha that broke RTK's `createSlice` types. Lenz tried to come up with a fix, couldn't, and had to add a workaround to check what TS version is being used and specifically use an altered type. Since there _isn't_ a good way to know what TS version is being used, Lenz resorted to hacking together a new package that abuses the `typesVersions` property to define a different TS type for _every_ TS major+minor version combo, and then used that to decide what the RTK type should look like conditionally [1].
Another pain point is debugging type transformations. I reworked the Reselect types in 4.1.x to do a much better job of inferring the argument types for the final selector, based on the intersection of all the input selector arguments. This ended up as a monstrous type that does a types-level map + transpose + intersection [2]. It took me weeks to get this working right, and I frequently had to break it down into multiple small intermediate types to see how TS was processing each step.
I know that someone on Twitter was recently working on an alternate TS type-checker based on bytecode, and they said they had some kind of a working types-level debugger [3]. Having something like that officially, where I could see each step of how TS was transforming the types, would be _hugely_ valuable.
There's a couple folks like AndaristRake who are able to dig into the internals of the TS compiler itself to trace how it's interpreting the types. I definitely don't have that ability :)
I agree wholeheartedly that debugging types is very difficult. The best we have is hovering our mouse over the definition to see what type was created from the definition. Obviously not great.
Also, debugging type differences with deeply nested objects (like what happens with graphql schemas) can be hugely painful. You need to copy the error message to its own file (since the errors can be huge) and debug what specific piece failed.
I do feel the Typescript documentation is lacking and the only way to get better is to read open source projects to see how others have done it… which is only helpful when they’ve solved the exact problem you’re looking for.
It’d be great to have a recipe book of common advanced typescript manipulations and how to compose them together.
Another way to phrase this is “statically typed languages suck if you want to write highly dynamically typed code.” That’s true. I think there’s 3 options here:
1. Keep writing code in a very dynamically typed style, despite choosing a statically typed language. Just deal with/stomach the extreme type complexity that is necessary to model your dynamic style statically
2. Keep writing code in a very dynamically typed style, and switch to a dynamically typed language
3. Stop writing code in a very dynamically typed style. If you’re using a statically typed language, write code that embraces static types
IMO 2 and 3 are both reasonable choices, but the author is deciding to choose 1, which is of course painful. I strongly disagree that libraries must be super dynamic though - that’s a pure style choice that some library authors adopt, but you can absolutely write basically any library in a static types friendly style, you just need to reflect that in your interfaces.
I guess the one thing that IS TS specific is that ppl can write their libs in JS, then try to add types to it, and if they chose crazy dynamic interfaces, it will be an incredible pain to statically type. But honestly, you could just say “this is a JS lib, there will be no TS types.”
> Another way to phrase this is “statically typed languages suck if you want to write highly dynamically typed code.”
Reminds me a lot of the NoSQL wars which eventually boiled down to 'Non-relational databases suck if you want to store highly relational data'. As ever, there are no silver bullets, only the right tools for certain jobs.
(Hoping to not invalidate anyone's concerns with this comment)
The majority of complaints I heard on Typescript were in the lines of: "I should be able to do this X thing that would not fly in a static language / not a good idea". Many times Typescript caught me doing stupid things like passing the wrong type or not checking data adequately, also forcing me to comment my code by describing shape of the data. It was a life saver for debugging, though it's not perfect, it's more like lipstick on a pig for a badly designed language.
Yes we need better tools as library authors but absorbing this complexity is our job. The more we take advantage of what Typescript can do the fewer types the end users deal with and the better experience they have.
Making your library's internals simple should be a non-goal. The number of people dealing with it will be a fraction of the people dealing with codebases using your library.
Libraries need good documentation with reference and code examples. What libraries don't need is complex type annotations that wont make it into the compiled code anyway. The worst documentation I've ever seen is the one that use the types as documentation, looking at you the Language server interface.
I think the author is good with naming symptoms and I think they miss the true culprit. It's not typescript that is causing this pain, no it's the pain of getting correct abstractions laid down in code. It's truly the pain of code design activity. Typescript and library programming is just forcing that pain to the surface instead of hiding it away.
I know this because I struggle against these "pain during code design" & "get abstractions right" forces when I program.
Declaring types is really declaring what domain objects your program cares about and function signatures are really just describing interactions that can happen. So what 'domain objects' should meaningfully exist in my program and how do they interact? A bad abstraction can burn up a lot of time, so make sure you check your abstraction. I typically check mine describing stories to my friends (or rubber duck) and ensuring I'm using plain English and the other person doesn't get lost. YMMV
When you're doing web client programming you're typically using a library to accomplish work, and much of that work is reacting to user input and rendering data. I would argue that because so much of the linguistic heavy lifting has been done for web client programming, practicing the abstraction exercise is done less than in other programming domains. Doesn't mean it couldn't, just trying to highlight a difference in programmer domains. This is why the pain is more apparent in library programming than web client programming.
I imagine that advanced individuals in code design understand more of the connection between math and programming and are capable of describing systems of interesting work in few simple statements. I hope to reach those heights someday.
I've always typed not only my types, but functions, programs, tests, everything, in every language I've ever coded in.
OK, sometimes I first write stuff (in pseudocode, or at least bad syntax) on paper first, but in the end I always type it into a code editor on the computer. Type type type, clickety click click clack...
I wish there were a formal subset of typescript that was go-like in design. Interfaces, structs, and simple types but not all the class-based object oriented complexity. A focus on composition instead of inheritance.
I like typescript but really worry that it's fueling a boom in unnecessary complexity and architecture. Are we going to look back at monster typescript codebases in the same way we look at monster java codebases riddled with abstract and redundant layers of complexity because the "design patterns" say to do it?
Like many here, I love typescript for library writing, at least internally. I've not made any big npm packages, so not sure how that would do, but I imagine I'd like it a lot too.
What I hate is getting the build system set up to generate the right combination of .js/.d.ts files to work with the build system of whatever I'm using it in, particularly for libraries that use any form of preact/tsx files. I must be seriously missing something, but after 5 years as a node/react developer I still suck at making build pipelines for new projects.
I find that most of the pain/complexity around TypeScript types happens when you're trying to write super dynamic or polymorphic JavaScript-style code and then put types on top. This comes out especially when adding types to an existing codebase. I can definitely see how this would affect library authors more than application authors, but I'm not sure it's TypeScript's fault, and I think it can be avoided by making different choices about what interfaces you expose
Or, worst-case, just weakening your types. Plenty of libraries have weaker types than they could have, but sometimes it's justified because it would be hard to get them exactly right
337 comments
[ 0.83 ms ] story [ 296 ms ] threadBut I firmly disagree with how the article begins.
> In effect, we are shifting complexity from end-developers to library developers.
That is the whole point in creating a library. To hide complexity in a nice easy to use interface/API.
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!):
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:
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.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.
> 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.
Whether the effort is worth it is, however, a totally different question to whether it's possible.
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.
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.
* 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.
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.
Do you also use == and != for comparisons by default?
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.
> .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.
You use an ambient declaration to declare the missing classish part of the type.
You see this pattern pretty often in DefinitelyTyped.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.
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.
> That is the whole point in creating a library. To hide complexity in a nice easy to use interface/API.
I think that's a fair point, but generating types that satisfy all use cases is very challenging to get right -- disproportionately so. I could see a world where -- without proper tooling and growing complexity -- typescript libraries becomes so difficult to maintain that people give up or burn out. Maybe that's a pessimistic outlook but I already feel that way some days.
If you write a library in TypeScript, determining what types are present as part of the interface is one of the very first thing that should be done.
Too broad a statement. There's loads of value in having a compiler figure out types for you after/when you write the code.
But granted, as a general rule you are correct. I was referring specifically to API interfaces.
If you're talking about figuring out what types you're going to accept, you should absolutely be defining that on your own up-front. If you don't even know what your types are how is an end user going to figure it out?
Totally agree. Minor ergonomic changes in a library have a wildly disproportionate impact on developers. If I save a developer 5 seconds with a better type, and I have 1000 developers using my library, that's 5,000 seconds I just saved - and that's assuming they only ever use my type a single time! With this in mind, I'm totally OK paying a tax on making libraries a bit harder to write, if it means that the benefits of stronger types fan out. After all, writing that type could take 4,995 seconds for me to write and still be a net positive.
And honestly, 5 seconds isn't even close to accurate. Good type definitions have saved me hours.
That is the whole reason to use libraries.
This blog post shows that types make development more enjoyable by shifting the complexity burden to library authors, who are vastly fewer in number than library users. It is a clear win.
I spend a decent amount of time in the redux world so `redux-toolkit` is a great library to see how types are done *correctly* in a real codebase. To be clear, they do a fantastic job with types, but the level of complexity is pretty startling.
https://github.com/reduxjs/redux-toolkit/blob/master/package...
That is just one example but the codebase is riddled with complex types. Also, when you look around, note the amount of types vs actual code.
It's pretty common in style guides to never nest ternaries. In typescript, that's the only way to narrow types based on other types. It's a mess!
Strongly typed shell, whatever works core.
In that vein, if anyone here can tell me how the hell you functionally map over a typed object by key in typescript I'll be eternally grateful.
I always wanna do something like Object.keys(typedObjName).map(...) but that doesn't work.
It's so stupid and small and minor and it's never really prevented me from getting something done but it drives me out of my mind that I still can't find any clear documentation on how to do it.
To get the typechecking back on the keys, you either need to declare the loop variable outside of the loop itself, or use type casting like this:
Neither is particularly clean.There's some boilerplate in the definition, but it's fairly clean and non-repetitive. And easy to use in the "client code".
https://www.typescriptlang.org/play?#code/KYOwrgtgBAKg9ga1AS...
const tokens = ['parenOpen', 'bang', 'plus', 'minus'] as const;
type Token = typeof tokens[number];
type TokenTable<T> = Record<Token, T> // alias for { [key in Token]: T }
const isToken = (t: string): t is Token => tokens.includes(t);
const patterns: TokenTable<RegExp> = { bang: /\+/, // rest }
https://jsfiddle.net/knvztxq7/ ^ that code will work, it outputs the key names to the console
https://jsfiddle.net/knvztxq7/1/ ^ that code... is also working even though I swear it hasn't worked for me before so now I think I'm actually just losing my mind.
Yeah wow I can't reproduce my error now... I'll come back here if I figure out what I did.
Is this a type space/value space thing? Like Object.keys(...) is always a string[] instead of Array<keyof TypedObj> like you might expect?
If not, it sounds like one core issue is 'adding types to an existing untyped codebase is hard'. This checks out. :)
> The kind of hoops I have to jump through to get types "just right" in a web app versus a library is dramatically different. It's rare in a web app for me to need constructs like conditional types, type operators, and overloads. As a library developer, they are heavily used. These constructs are highly dynamic and embed logic into your types. This leads into my next frustration with typescript which is debugging.
I would have like to see some examples of "conditional types, type operators, and overloads" in action, and an argument for why these constructs are so much more prevalent in library code than in application code.
I don't have a counter-argument, and I don't have any reason to doubt the author's insight, but after reading the article I don't feel like I have an increased understanding of the problem-space.
Why are they more prevalent in library code: to make changes non-breaking (or breaking, but fixable with minor tweaks). Of course types can also be a solution to this problem, by promoting consequences of incompatible change from runtime to compile time, but that only holds if you can assume that all calling code is type checked.
Before judging it as "terrible" it would be good if author has more constructive criticism - if typescript is "terrible" what is not "terrible", haskell? What are specific things that can be changed to make it simpler? What if typescript is actually very good but the underlying type theory is just inherently complex?
You also have to appreciate what kind of value it brings to library users - and imho, in many cases, the value is so high that the whole discussion is a nobrainer.
In my opinion - if you want to use typescript, you need to learn it, especially when writing libraries. It doesn't make sense to want to use something and not want to learn it at the same time.
However, I think that saying it is "terrible for library developers" is a bit far. I think its terrible for developers who want to make use of advance types... which ultimately doesn't depend if you're a library dev at all.
It boils down to: "Typescript learning curve gets really steep after the basics"
The author mentions they help maintain redux-saga. I don't want to dig on them, but from my personal experience, that library takes you down quite an opinionated application path with complexity inherit to it. Heck, just making types for redux was a pain, let alone adding async and additional composability.
My take is that the author has chosen complex tech to work with, and that influences the complexity in their types.
[0] https://news.ycombinator.com/item?id=32569708
[1] https://cdecl.org/
Much of the complexity of TypeScript’s type system is a direct result of excessively dynamic interfaces in existing, untyped JavaScript. Starting with types is a good way to catch that early, rethink the interface, and either isolate it or avoid it entirely.
Damned if you do, damned if you don't.
However, if you look behind the curtains of these infamous gripes (some of them so often repeated they are cliché), 99% of the time its people who just haven't spent enough time around a certain framework or language and go create a rant post (similar to OPs post)
You're free to love the tools and languages you use all the time. But it's bad practice to go around bad mouthing tools and frameworks you don't full understand or haven't used productively.
I made a similar comment in 2019 about "TS pushes you towards simpler APIs" [0], so I agree that it's both a good thing in principle, and something TS usage leans towards in practice.
At the same time... JS _is_ a very dynamic language, library design reflects that, and library code by its very nature must account for the different ways that users will want to call it. That causes library types to be much more complex than app types.
[0] https://blog.isquaredsoftware.com/2019/11/blogged-answers-le...
I would argue it should not. If your API cannot be well expressed in the Typescript type system, it is your API that should change.
Especially since types are documentation. If you are using some meta dynamic type garbage, then I as a user of the API am left to wonder "Ok, this takes <U, K extends keyof U>... WTF is it expecting?"
This isn't to say that sort of thing is ALWAYS wrong, but rather it should be the exception and not the rule.
You do types because constraints make code clearer. By having a BF compiler in the middle of your template definition, you've defeated the types and you might as well put an `any` there with docs that say "here there be dragons!"
I think TS is great in certain cases- I love being able to get intellisense info from TS. When I download a new library and I get those helpful hints built right in from the libraries TS adoption, I love it. Typescript in this instance is reducing friction in my development workflow.
In my day job I work at an agency where we rapidly deliver a lot of new web properties. I have used Typescript on projects when it was a client requirement, but in our environment using Typescript generally feels like it overall adds friction and time to the project. Some team member inevitably gets stuck spending a bunch of extra time working with TS stuff: setting up the project with proper rules, writing some complex interface to deal with some random API we are either consuming or creating ourselves (in which case it is usually under rapid prototype development and needs to be updated constantly), etc.
I'm sure it is very helpful when working at scale with tons of developers on a highly stable and established project, but in my opinion it isn't generally applicable to every web property that it would be better on Typescript; it really depends on the scale of the project and the scale of the team maintaining it.
This sounds to me like MongoDB advocates like "psh! who needs a schema! don't slow me down, I'll just dump JSON into the database however I like." The types exist whether you write them down or not.
I agree with you that TS might not be great for everyone and every use case. But maybe you should just use `any` some more!
Yet in JavaScript I see this all the time - pass a string URL, but we also accept a function which will return a string URL, or maybe an option object - { url: string, strict: boolean }
So I think it's more of a culture thing.
For example: every object’s prototype chain ends with `Object.prototype`, so all objects have a `toString` function. I shouldn’t be forced to narrow my `number|bigint|string` (for example) just to do `x.toString()`. If I was, we’d get this ugly and bloated mess:
Regardless, they’re just copying JavaScript’s duck typing feature. It’s not much different than languages like Java and C# that have interfaces. In C#, if my parameter is IClonable, I don’t care what it is, but know there’s a `Clone()` method attached to it.In general, if someone with experience in a domain is complaining that something is hard, it's safe to assume that the thing is actually at least kind of hard, and that they aren't just a crybaby about indentation and formatting.
I also assert that if NDFrame.loc were less polymorphic (e.g. if you had a separate accessor like NDFrame.mask for boolean subsetting) it would be easier to use, easier to reason about, easier to teach, easier to document, and easier to write type hints for.
The worst situation is when you want to have multiple return types depending on the argument values, like Pandas' inplace=True. Putting aside the problem of dependent types, even supporting only literal True and False requires you to write out every single overload twice, creating a literal combinatorial explosion of overloads.
And that's also ignoring the whole world of parametric polymorphism required for higher-order functions, factory functions etc.
Maybe the situation is no better in something like C++, but I don't know enough C++ to have an opinion on it.
I’d argue that it has to do with C extensions making it a lot of work to get it right.
Some library code does it (which is where I learned how to do it ‘correctly’) but in general, run of the mill code it is pretty rare. Just something like an overloaded __add__ turns into a page and a half of code and there’s a lot of functions in the numbers protocol struct.
Labor of love really.
This is mostly an issue if you're tacking on those types on top of your existing design, rather than starting your design from your types, no?
Part of this hook is that users can pass in custom data. I tried to create a type that was basically "an object of any string key, with the type value". Gave up after like an hour of banging my head against my desk. Maybe I'm new to TS, but FFS it should not be that hard to do.
I’ll define 50 strongly typed constructs over having to run my code through 50 times to catch all the runtime bugs.
It accepts, as a parameter, a URL for Apple's REST API. My library handles authentication, and returns the parsed JSON result, with a handful of tweaks to make the API more usable in JavaScript.
Depending on which URL you request, you'll get different result object back. You could get a single object in response, or an array of objects, and the type of returned objects is different for each URL type.
How would you add TypeScript types to this API? Well, Apple provides an OpenAPI documentation of all of their URLs, which I could use to autogenerate types, but then, how would I handle all of those types in response to the user's string input?
Well, it turns out that TypeScript is so amazingly fancy that you can write very clever code to parse strings at compile time, extracting parameter types etc. from string literal types. https://lihautan.com/extract-parameters-type-from-string-lit...
The documentation explains how an API like this:
can parse its parameters into a Request type with shopid, itemid, and args[] array parameters. This would catch a bug if you had a typo, e.g. "itmid".But the code to do that looks like this:
Nifty, eh? But, as the article says: how would you test this code? How would you debug it?Clearly, I wouldn't do that. Instead, I'd write a script to autogenerate individual methods, e.g. instead of get(`apps/${appId}`) that returns a parsed JSON blob, I'd autogenerate a getApp(appId) method that returns an App object.
But that API isn't any simpler than the API I already have; it's just different.
And let's not forget that I'd have to write a script to autogenerate these methods (or just their types) based on Apple's OpenAPI specification, and now I have to maintain that code, updating my @types/node-app-store-connect-api definition every time Apple introduces a new URL you can request. Testing and debugging that is a challenge in its own right.
And even if I did it, the complexity of my library just went from a few hundred lines of glue code to 1,000+ lines of type generation (plus tests for the generated types).
This is in no way worth it for me. As a library developer, adding TypeScript types would make my life harder.
I would say that the increased complexity of your project would accurately reflect the complexity of the underlying API
> As a library developer, adding TypeScript types would make my life harder.
Yes, but it would mean that the various parameters passed to your library would now be type safe, which sounds worth the work
That string-parsing thing isn't in TypeScript's documentation at all; it's a random blog post. I'm lucky I found it. TypeScript's site should document how to do this.
Furthermore, it should be easier to debug parsed stringly-typed APIs using the `infer` keyword. Today, I just run the compiler over and over again until it stops throwing errors, and the errors are really not very helpful.
There should be a standard tool to help me test that I used `infer` correctly, ideally helping me by fuzzing the code to see if I allowed something I shouldn't have allowed, or blocked something that I should have allowed, and to test against various versions of TypeScript.
Narrowing types shouldn't be done just by nesting ternary expressions. As the article notes, "It's pretty common in style guides to never nest ternaries. In typescript, that's the only way to narrow types based on other types. It's a mess!"
Lastly, this idea doesn't sit right with me:
> I would say that the increased complexity of your project would accurately reflect the complexity of the underlying API
I designed my library so it doesn't need to incorporate that complexity. Users of my API have to learn the API, but they can learn it from Apple, by reading Apple's documentation, and by inspecting the objects that Apple actually returns (rather than what their OpenAPI specification says they'll return).
Incorporating that complexity into my library will make my library harder to work with, more difficult for others to contribute to my library. As the article says, "Types make it much harder to maintain a js library, and especially difficult to contribute to them."
TypeScript is forcing me to duplicate the API's complexity in my code. Even if you think that "sounds worth the work," I think you have to agree that the work load is much higher than the work I've already done.
This paragraph kind of sums up both the OP and this whole HN discussion of it. So I'll throw my comment on here, although I suppose it could go pretty much anywhere.
Anyway, one way to read it all is: This just goes to show that
1) JavaScript sucks;
2) Therefore the whole world of "Web apps" built on it is an unmaintainable buggy nightmare;
3) Trying to graft "strong" (not really) typing on top of it in the shape of TypeScript is just polishing a turd.
It's like somewhere around 1995/2000 the world of computing jumped through a time warp back to 1975, and we've been stuck there, in a world dominated by the new BASIC, ever since.
___
Edit: Typo fix, *"domination" --> "dominated".
This sounds awful to me. Why wouldn't it be separate functions? I absolutely would not want that at all as an end user, I'd find it confusing and frustrating.
Furthermore, if Apple introduces a new function, I'd have to release a new version of my library to support it. Today, my library can handle any object Apple's API supports, without upgrading.
As it stands, the API works like this. It's fine.
Had redux been developed in typescript from the start, I doubt it would have chosen this API. Then again, the current version is already in typescript, so perhaps that optimism is unwarranted; unfortunately my experience with it is years ago already.
This isn't part of the Redux core package, but it's a convention the community adopted shortly after Redux became popular. Redux Toolkit uses that as the standard structure for an action object. `createAction` defaults to just `{type, payload}`, but you can optionally provide a callback that adds the other `meta` and `error` fields. So yes, there's some additional complexity here because of the optional field contents, and this type is telling TS what the final type should look like based on which fields exist.
Note that as an end user, you normally don't even call `createAction` yourself - it's automatically called as part of our `createSlice` API. What end users normally write is:
How do library developers debug their highly dynamic and heavy use of conditional types, overloads?
Don’t do that. It dramatically increases compile time and results in a maintenance nightmare. Keep types primitive.
In my own code I use type unions of primitives and/or named types. For objects I use interfaces. With that I am able to provides types for about 98% of my code. That left over 2% is generally for extending the global Array type or extending an event.
I wrote a set of React components that gets 46,000 npm downloads a month and typescript is a godsend. My library is a bridge between two heavily popular projects, so my dependency tree is fairly intertwined. The library solves a real user problem and does it efficiently. It isn't totally perfect, but it covers 98% of the use cases.
I wrote comprehensive tests as I developed everything. I have good documentation. I have an example app people can play with. I have codesandboxes. Yes, it was a lot of work, but that's what building products is all about.
I've maintained this library for over 3 years now. I've upgraded it many times as the underlying dependencies have changed. A few times with backwards incompatible changes. I get outside contributors doing great work.
I do releases on a regular basis, without worry, because my test suite is that good and because I know that if the underlying types change, tsc will catch that too. These things have saved me countless hours of work finding bugs and fixing issues.
In other words, I call bullshit on this entire blog post. Ignore it. Typescript is the correct solution. What is the alternative? Just using JS and letting things break without knowing it? Come on.
Learn how to use types. Learn how to use a professional IDE, like IDEA, that shows you what's wrong as you develop. Learn how to write tests. Learn how to use a debugger. These are all things any decent developer should know and practice on a regular basis.
That's the title. That's the whole premise of the post.
> his primary gripe specifically as a library developer - is that the knowledge base for library developers is very sparse.
No, it isn't.
I agree with GP, you don't seem to have read his article and knee jerked a reply.
And all you have to do to prove your point is to link some great documentation explaining how to make complex types for libraries and how to test them.
Which you haven't.
I feel that my point is proven with a successful multiple year open source Typescript library.
Great documentation? That's subjective. I'd start with a deep reading of the official docs as they are quite good: https://www.typescriptlang.org/docs/
That only proves that it's possible to do, not that the language facilitates it or that certain modifications to the language/ecosystem wouldn't greatly improve the process. The point of the article is the latter, not the former.
No. What you said was:
> In other words, I call bullshit on this entire blog post. Ignore it.
I expected to be downvoted with the rest of the thread, but somehow got flagged. That's a first.
And if types change, and it still works, it's basically working by accident.
I don't consider that working.
If you write comprehensive unit tests for your javascript, that should effectively also work too... this is what ruby developers pretty much had to do.
We can see that over time based on the decline of ruby popularity in general, that fell out of style though. It is too easy to make a mistake and everyone was just duplicating what a compiler does for us.
I can't tell you how many hours I would sit there pair programming with other ruby developers just trying to figure out what 'type' an object is. Mind boggling, but it was good to get those high paid consulting hours.
And to be completely clear: you can have safe polymorphism without type hierarchies via things like type inference.
So the Liskov substitution principle now passes off as "working by accident"?
Whenever I find those guys on Github with several maintained multi-thousand star projects, I wonder "how do they do it?" (fund themselves + work on OSS). I am always curious about the underlying life stuff
Thanks in advance!
In this case, I needed the code for an internal project and decided that the company would benefit the most from making it a library and getting outside help on it. It was an itch that I knew many other people needed scratched because there was a competing project that had gone silent and was incomplete/poor quality. That justified my time spent... I had to write the code either way, might as well make it open.
When I look for people to hire, and I've done a lot of hiring over the years, I primarily look for people like myself who put that extra effort in. Either in their own personal projects or by at least contributing to something upstream. Even just simple documentation changes is something I'd hire over. I fully understand that people have lives outside of work, but honestly... if you're going to be a top developer, you've got to put in a top level of effort, because other people around you are.
In my experience, only about 20% are.
Surely that's not the only alternative.
Here's a few off the top of my head:
- Being able to write complex types in a syntax that more closely resembles JavaScript. Using Array length for counting or nested ternaries for if logic gets old fast.
- Being able to debug types, not console output, a real debugger. See: https://twitter.com/MarcJSchmidt/status/1539787500788613120
- More comprehensive documentation on writing advanced types
- A typescript specification
That is really nice. I'd love to see that live.
Yikes. This is how you end up in preprocessor hell. Macros are generally not a good thing. (Fyi: TS types are already Turing-complete which is arguably a mistake.)
> Being able to debug types, not console output
Eh. It's not like Java has a "type debugger." Why is this needed? Why are your types so complex? Weird ask.
> More comprehensive documentation on writing advanced types
Really beating the same drum here.
> A typescript specification
What does this mean? We have a pretty clear typescript spec[1].
[1] https://github.com/microsoft/TypeScript/blob/main/doc/spec-A...
Not suggesting macros, rather an alternative way to define types. A function that accepts and returns types. Seeing as typescript has a JavaScript interpreter I thought it might be feasible but I'm just spitballing it.
> Eh. It's not like Java has a "type debugger." Why is this needed? Why are your types so complex? Weird ask.
I don't buy this argument. Writing any type of complex types with recursion, arrays or ternaries sucks and it will take more than "this is how Java does it" to convince me otherwise. I like to debug with a debugger, not my head.
Take a look at the examples in the article to see some complex types and read some tweets from library authors complaining on twitter. I can find some examples if you're interested.
> We have a pretty clear typescript spec
I hadn't actually seen this and it looks interesting but seems pretty out of date, Typescript 1.8?
The article (and ensuing discussion) are about _library_ code, which necessarily has types that are orders of magnitude more complex than application code (source: see some of the links in the article).
The reason this isn't a problem in Java, C# etc is that the things these libraries do simply _aren't possible_ in the Java type system, so no one does them, and users end up with (usually) clunkier libraries.
Now there's definitely a question of when you start hitting diminishing returns but you can definitely do so much with and extract confidence from very strongly typed languages.
Really? This is fascinating, is there anywhere with more on this?
Are you somehow implying that C++ or Java don't have libraries? As a sidenote, React (and that entire ecosystem) is an architectural hellscape (see Vue for much more streamlined & cleaner code).
I'm also not necessarily saying this is a good or bad thing overall, since it can so easily lead to extra complexity.
FWIW, I'd choose C#/Rust/Kotlin/etc over TS 100% of the time for projects that don't need to run in a browser.
It seems like you and I read a different article. The post I read did not advocate for going back to JS; rather it advocated for better tooling and documentation (with examples, ideally) for library developers
I developed a library used by the React app I work on to cache network results in localstorage (to reduce the number of expensive requests made on page refreshes) and generate React-Query query-functions to read the result from localStorage first before fetching from the network as a fallback (if the data is stale or uncached).It's not an especially large library, but I felt all of the pain points the author describes. It's great that you didn't with your library (and I'm sure there are large classes of libraries whose authors wouldn't feel these pain points), but it's a very real issue if your dealing heavily with generics, serialization/deserialization, and/or complex type interactions
I picked out this file pretty arbitrarily: https://github.com/TanStack/query/blob/main/packages/react-q...
The author of react-query seems to be very clear at communicating how the library works and the library itself provides a great developer experience, but it's also an example of how much work can go into correct typing in library code.
An example from the docs with middleware:
https://docs.pmnd.rs/zustand/typescript#middleware-that-chan...
If anyone calls this “easy, just learn generics” I won’t believe them further.
Typescript is a horrible as you make it.
Many of these things are easy to do in JS because JS wasn't built to be static typed object oriented language. Many of these are hard to type because the underlying language is so permissive.
As library authors the desire is to be as permissive as possible, to use the "simplicity" of the untyped JS language to express an API surface that accepts any combination of possible inputs and does the greatest amount of work with that. There are many "JS native" libraries that do that, including JQuery's $ "operator" that was a swiss army knife of a million different tasks all using a single constructor which created object instances that any number of plugins mutated over time. As a JQuery user that was a very easy experience to work with, it's permissivity felt like simplicity and easy-to-learn. As someone who briefly spent time debugging JQuery types definitions in Typescript, that was an incredible nightmare. (That was also many, many versions of Typescript back with fewer typing tools, many of which would have helped a lot, but also made everything even more complex than it was at the time.)
I do feel like a lot of library authors sometimes need to ask themselves as types grow more complicated in their libraries if the trade-offs are worth it. That "simple" API they are trying to give their users, is there a more "complicated" API with simpler types to use instead? Sometimes that's actually the simpler API to use understand too, but many of your end users see your types indirectly and simpler types in the API are simpler experiences of those types.
The article mentions needing to add lots of overloads and that's specifically something I'm thinking about there. Another example in this thread included a function that could take a set of arguments as either an object ({ a?: thing, b?: someOtherThing, c?: thirdThing }) or tuple ([a, b, c]: [thing, someOtherThing?, thirdThing?]) and in both cases many of the parts were optional, complicated further by the thing type being generic itself and the tuple accepting different orders of parameters. Can you just pick one, object or tuple for your API? Users have "less freedom" to do as they wish and the result seems less "simple", but you can eliminate so much complexity in your types. If you do still need both ways, maybe it's reasonable to split from "overloads" to different functions setThingsObject(things: { a?: thing, b?: someOtherThing, c?: thirdThing }) versus setThingsQuickly(things: [thing?, someOtherThing?, thirdThing? ]).
It's easy to armchair quarterback refactor other people's APIs, of course, but it's certainly a factor when I'm building my own APIs: These types are starting to get complicated here, should I refactor to simpler types? Should this API be split into two different/distinct endpoints to simplify the type signature? And so forth.
Some complexity is unavoidable, of course, but sometimes it is worth trading "simple, permissive JS with incredibly complex types" for "tedious explosion of JS that looks complex at first glance with incredibly simple types". It's a deep trade-off space to explore and what's right for any individual project is deeply personal opinion.
I think it really depends.
[1] https://rematchjs.org/docs/getting-started/typescript
I get playing with technology for side projects, but for something commercial there is no way I would sign off on including that package into a project.
This is what types look like when you're on the library side. The post made very clear they aren't talking about the app dev side. The library types are complex like this so that the app devs have a smoother experience.
I really wish Typescript had a great pattern matching system. It would be very nice indeed for structural typing, everything would be easier to read, probably some ways to add more power/expressivity, etc.
eg
becomes or even, to make the top level way easy to understand in this case: where Part1Match etc are themselves pattern matches, so you can compose them like functions.The expressivity makes it possible for library authors have IntelliSense give more useful suggestions to the consumer, make automatic inference work in more places for the consumer, and make more illegal usage be impossible, at the expense of complicated type constructs. But of course all these is somewhat optional.
The entire article persecuted TS because the author wanted to learn quicker with little effort. Writing TypeScript in a node, app or library makes no difference. It is a language, NOT a framework.
I completely disagree, and I was surprised by this sort of comment.
Find me a single programming language whose docs are as good as TypeScript's docs and reference. I'd be surprised if you could come up with a single example.
I think you dismissed the actual issue he faced- gen type defaults, which I just looked is still not actually documented, it's just in the release notes.
Don't get me wrong, TS is my favorite language, but let's not gloss over things.
Recently learning rust (spare time) and c++ for work. The documentation for these 2 are awesome!
PostgreSQL has great docs too
I think we are better at writing docs for programming languages than we are at libraries and frameworks. Maybe because languages are more ‘fixed’ and less likely to change so over time they just get better and better.
It's a matter of having the skill and the will though, and I feel like a lot of projects simply don't emphasize documentation. Even so, not everyone's going to be a good technical writer.
Vue has excellent docs imo.
But in terms of technical writing being a skill. I agree. I appreciate the effort people put into docs. I try my best but I’m not very good. So I have a lot of respect for anyone who can sit and write excellent docs.
There are lots of ways to do things that you kind of just have to learn from reading blog posts, or reading code and then asking in the discord when you see something undocumented (because have you tried googling "what does <T=...> in Typescript mean"?).
Ideally, at the very least, all syntactic features and keywords of the language should be documented, but it's more than that; Typescript is a metalanguage, and authors of many libraries have also developed patterns which are essential for describing complex types. Some of these patterns are documented (for example, discriminating union), and some are not (for example, opaque types). And some features which are documented could do with a lot more exposition and functional examples ("as const"), or notes on when to avoid (enum).
Search “typescript generics syntax” and you get the excellent TypeScript docs on generics as the first result. It covers generic constraints.
Indeed, but I'm not sure how that's relevant since it doesn't cover generic parameter defaults.
Golang entire standard library is documented. Plus with how the language is designed docs don’t need to change with new versions. Just with new functions.
This is a non sequitur. Typescript can be bad even if other languages are worse.
The argument being made in the article and this discussion isn't "use X instead of Typescript" it's "Typescript should be improved".
The former is vacuously true once you know it is an option. Everything would be better if it could be improved.
The latter is subject to questions about cost. If Typescript had best in its class docs, that would imply that other communities had failed to find or execute a practical option to turn time/money into docs improvements. That would be moderately persuasive evidence that Typescript's resources are better spent elsewhere.
---------
On an object level: Django and the python community tend to have great docs.
Everything spelled out. No "you must read the whole thing or have worked with it for years to understand page 5".
Plenty of inline examples and even more user submitted ones (although as with all user generated content, some caution should be used when reading those).
But I am fairly certain that like the Perl documentation and unlike Java and Spring docs none of them was infuriating.
In fact, now that https://tanstack.com/query/v4/docs/plugins/persistQueryClien... appears to be stable I'm thinking it might even be good to migrate to this (though it appears to require multiple queryclients if you want different cache parameters)
I would even say: most libraries used for just about anything, are written in C/C++/Java, all of which are typed languages.
The post's author simply lacks the education and experience to actually have a valid opinion on this topic.
Besides your take being just outrageously exclusionary – the author is Eric Bower, a principal engineer who's also a Redux Saga maintainer. They're more than qualified to weigh in on this.
I’ve seen some crazy assed stuff they do with C++ templates and my reaction isn’t that they need to be spending their time on writing blog posts to explain the dark mysteries to me but that maybe I’m not the one who should be trying to write a library without spending a massive amount of time learning how this stuff works.
Sure, I can beat a template into submission using the famously helpful error messages the compiler provides and some random blog posts by people who actually understand what’s going on and I’m very thankful that they spent the time explaining it. Even having 1000 line long error messages helps once you learn what it is really complaining about. And this isn’t even my literal job, just something I do because I enjoy it.
IMO, the author could have done better by providing more concrete examples for the points of criticism, and understating the conclusion or moving it to the end or dropping it entirely, as I think it's better to give the reader the choice to make up his own mind, given the facts and evidence.
Just because I disagree with their premise (tool writers need to take responsibility for my lack of knowledge) doesn’t mean I think they can’t talk about it.
I mean, if they weren’t talking about it I’d have nothing to disagree with and have to do something productive.
And, you know, appeal to authority isn’t a valid counter argument…
The things the author talks about aren't even remotely possible in the type systems of pretty much any other mainstream language, so I don't know how you could interpret this as a "TS bad" post if you read past the headline.
"Lean how to use my types!"
Any given project uses a large number of libraries.
The point of a library is that it's 'useful'.
When a large number of libraries use vague and differing typings, it becomes impossible to manage.
The fragmentation created by the agile nature of Typescript is, predictably, a giant problem.
My point is that I've written a library that gets a lot of use. It has decent enough APIs. Nobody is writing blog posts like this about my stuff.
How? None of what you are talking about directly relates to the article. It feels like you are just reacting to the title and not the contents.
For instance, you say:
> I call bullshit on this entire blog post. Ignore it. Typescript is the correct solution. What is the alternative? Just using JS and letting things break without knowing it? Come on.
But the article says:
> I love typescript and think the team working on it are incredible. Typescript has completely changed the FE landscape and wouldn't want to dismiss its contributions.
> But as a library developer, we need:
> - better documentation,
> - better tooling, and
> - to spend less time making tsc happy.
This is not an article advocating for “just using JS and letting things break”, so why are you reacting as if it is?
Because the tone of the article seems to be that Typescript is horrible, and the tenor seems to be that the author is blaming his own incompetence on the language.
He has a point hidden in there somewhere, but I needed the HN comments to point it out to me.
Summarizing the sections:
- Documentation: there's docs on how to use the language as a whole, but nothing focused on writing libraries (how to build/publish, type tricks that libraries may need that apps don't, etc)
- Debugging: debugging types is a nightmare - you basically have to hover over temp variables to see what intermediate values are
- Complexity: TS types for real-world lib code can get complicated
- Testing: testing types is necessary, and the community has had to invent tools to help with that
- Maintenance: maintaining types and responding to issues takes up time
None of those are comments that "TS is horrible". Those are all straightforward concerns with the experience of writing and maintaining a TS-typed library, and as a Redux maintainer I can back up every single one of those. I actually did a talk on "Lessons from Maintaining TS Libraries" a few months ago, and I hit all of those points myself:
https://blog.isquaredsoftware.com/2022/05/presentations-ts-l...
> Documentation: there's docs on how to use the language as a whole, but nothing focused on writing libraries (how to build/publish, type tricks that libraries may need that apps don't, etc)
Why would there be? Building and publishing libraries is a npm thing, and has nothing whatsoever to do with Typescript. Type tricks that libraries may need? If you are going to write a section like that it’s pointless to write it about libraries in specific. There is nothing about building libraries in specific because Typescript isn’t about building libraries.
> Debugging: debugging types is a nightmare - you basically have to hover over temp variables to see what intermediate values are
Sort of, if you’ve managed to make your types complex enough that you cannot reason about them. But then again, I don’t know of any other language that feels the need to provide a type debugger. The fact that people working with Typescript think they need it is either testament to their competence, or the ridiculous constructions they make.
> Complexity: TS types for real-world lib code can get complicated
Well, yeah. But it’s exactly as complicated as you make it.
> Testing: testing types is necessary, and the community has had to invent tools to help with that
I don’t know of any other language that thinks testing types is necessary. The big issue in Typescript is that types don’t guarantee anything about runtime behavior.
> Maintenance: maintaining types and responding to issues takes up time
Well yes. Maintaining any library in any language takes time. Do people think this is unique to Typescript?
I guess my own point mainly comes down to no other language having any of these things, and the OP’s idea that it’s on the TS authors to provide it feels misplaced. It’s not core to the language, and a lot of the problems are self inflicted.
The blog post/slides were interesting though :)
Per the OP's article and my slides, there _are_ many aspects of maintaining a lib that are unique to TS above and beyond a plain JS lib, and it would be really helpful to have additional docs and tools available to help with our use cases.
https://twitter.com/FPresencia/status/1545932895126175746
Agreed that writing TS types for libs can be a pain. I actually did a talk recently on "Lessons Learned Maintaining TS Libraries" [0], where I talked about some of the techniques we used, and some possible TS changes that would be helpful for us as maintainers.
As one recent example, TS made a change in a 4.8 pre-alpha that broke RTK's `createSlice` types. Lenz tried to come up with a fix, couldn't, and had to add a workaround to check what TS version is being used and specifically use an altered type. Since there _isn't_ a good way to know what TS version is being used, Lenz resorted to hacking together a new package that abuses the `typesVersions` property to define a different TS type for _every_ TS major+minor version combo, and then used that to decide what the RTK type should look like conditionally [1].
Another pain point is debugging type transformations. I reworked the Reselect types in 4.1.x to do a much better job of inferring the argument types for the final selector, based on the intersection of all the input selector arguments. This ended up as a monstrous type that does a types-level map + transpose + intersection [2]. It took me weeks to get this working right, and I frequently had to break it down into multiple small intermediate types to see how TS was processing each step.
I know that someone on Twitter was recently working on an alternate TS type-checker based on bytecode, and they said they had some kind of a working types-level debugger [3]. Having something like that officially, where I could see each step of how TS was transforming the types, would be _hugely_ valuable.
There's a couple folks like AndaristRake who are able to dig into the internals of the TS compiler itself to trace how it's interpreting the types. I definitely don't have that ability :)
[0] https://blog.isquaredsoftware.com/2022/05/presentations-ts-l...
[1] https://github.com/reduxjs/redux-toolkit/pull/2547
[2] https://github.com/reduxjs/reselect/blob/v4.1.5/src/types.ts...
[3] https://twitter.com/MarcJSchmidt/status/1539787500788613120
Also, debugging type differences with deeply nested objects (like what happens with graphql schemas) can be hugely painful. You need to copy the error message to its own file (since the errors can be huge) and debug what specific piece failed.
I do feel the Typescript documentation is lacking and the only way to get better is to read open source projects to see how others have done it… which is only helpful when they’ve solved the exact problem you’re looking for.
It’d be great to have a recipe book of common advanced typescript manipulations and how to compose them together.
1. Keep writing code in a very dynamically typed style, despite choosing a statically typed language. Just deal with/stomach the extreme type complexity that is necessary to model your dynamic style statically
2. Keep writing code in a very dynamically typed style, and switch to a dynamically typed language
3. Stop writing code in a very dynamically typed style. If you’re using a statically typed language, write code that embraces static types
IMO 2 and 3 are both reasonable choices, but the author is deciding to choose 1, which is of course painful. I strongly disagree that libraries must be super dynamic though - that’s a pure style choice that some library authors adopt, but you can absolutely write basically any library in a static types friendly style, you just need to reflect that in your interfaces.
I guess the one thing that IS TS specific is that ppl can write their libs in JS, then try to add types to it, and if they chose crazy dynamic interfaces, it will be an incredible pain to statically type. But honestly, you could just say “this is a JS lib, there will be no TS types.”
Reminds me a lot of the NoSQL wars which eventually boiled down to 'Non-relational databases suck if you want to store highly relational data'. As ever, there are no silver bullets, only the right tools for certain jobs.
The majority of complaints I heard on Typescript were in the lines of: "I should be able to do this X thing that would not fly in a static language / not a good idea". Many times Typescript caught me doing stupid things like passing the wrong type or not checking data adequately, also forcing me to comment my code by describing shape of the data. It was a life saver for debugging, though it's not perfect, it's more like lipstick on a pig for a badly designed language.
Yes we need better tools as library authors but absorbing this complexity is our job. The more we take advantage of what Typescript can do the fewer types the end users deal with and the better experience they have.
Making your library's internals simple should be a non-goal. The number of people dealing with it will be a fraction of the people dealing with codebases using your library.
I know this because I struggle against these "pain during code design" & "get abstractions right" forces when I program.
Declaring types is really declaring what domain objects your program cares about and function signatures are really just describing interactions that can happen. So what 'domain objects' should meaningfully exist in my program and how do they interact? A bad abstraction can burn up a lot of time, so make sure you check your abstraction. I typically check mine describing stories to my friends (or rubber duck) and ensuring I'm using plain English and the other person doesn't get lost. YMMV
When you're doing web client programming you're typically using a library to accomplish work, and much of that work is reacting to user input and rendering data. I would argue that because so much of the linguistic heavy lifting has been done for web client programming, practicing the abstraction exercise is done less than in other programming domains. Doesn't mean it couldn't, just trying to highlight a difference in programmer domains. This is why the pain is more apparent in library programming than web client programming.
I imagine that advanced individuals in code design understand more of the connection between math and programming and are capable of describing systems of interesting work in few simple statements. I hope to reach those heights someday.
If you find yourself thinking you need to test your types, you've taken a wrong turn somewhere.
I've always typed not only my types, but functions, programs, tests, everything, in every language I've ever coded in.
OK, sometimes I first write stuff (in pseudocode, or at least bad syntax) on paper first, but in the end I always type it into a code editor on the computer. Type type type, clickety click click clack...
It means that types are not subject to tests much like you don't test your tests.
types / tests are usually written in a denominational way and keeps the operational semantics of the program in check.
I feel i spent more time with that than with the development of the code ;-)
I like typescript but really worry that it's fueling a boom in unnecessary complexity and architecture. Are we going to look back at monster typescript codebases in the same way we look at monster java codebases riddled with abstract and redundant layers of complexity because the "design patterns" say to do it?
[0] https://github.com/preludejs
What I hate is getting the build system set up to generate the right combination of .js/.d.ts files to work with the build system of whatever I'm using it in, particularly for libraries that use any form of preact/tsx files. I must be seriously missing something, but after 5 years as a node/react developer I still suck at making build pipelines for new projects.
Or, worst-case, just weakening your types. Plenty of libraries have weaker types than they could have, but sometimes it's justified because it would be hard to get them exactly right