What compose() does is not the point here, it's that the type definition is totally unreadable. I was trying to figure out what compose was supposed to return (the function or the value), in that case, and I still don't really know.
Another random example from Axios:
<T = V>(onFulfilled?: (value: V) => T | Promise<T>, onRejected?: (error: any) => any): number;
Or eslint:
type Prepend<Tuple extends any[], Addend> = ((_: Addend, ..._1: Tuple) => any) extends (..._: infer Result) => any
? Result
: never;
Here's another real example from today... I was trying to figure out how to type "the name of this type's key has to be one of the following strings in this enum, but the type doesn't need to have all the keys". Here's a Stack link with the right answer: https://stackoverflow.com/a/59213781, but it wasn't easy to figure out. At first I thought it would be `[key in Partial<MyEnum>]`, but nope. Maybe optional? `[key in MyEnum]?` kind of works but fails in an new way (see the Stack for details). The correct way to do it is apparently `Partial<Record<MyEnum, unknown>>`, which I NEVER would have been able to figure out. Why the record? Why the unknown? Who knows..?
Don't get me wrong, I love TypeScript for the simpler use cases, and a lot of it IS that, thankfully. But the more complex compositions, especially in popular third-party libs? I've given up lol.
The use of single-letter keywords (K, T, V, P, R, etc.) combined with confusing re-use of punctuation (<> and : and () and []) that mean subtly different things depending on where they're used, on top of how JS already uses them, makes it even more so. Sometimes I wish TypeScript were more verbose and opted for longer, clearer constructs rather than stacked shorthands...
> I was trying to figure out what compose was supposed to return (the function or the value), in that case, and I still don't really know.
It returns a function. The one that's equivalent to applying the arguments in reverse order. I think that this signature is pretty clear for anyone experienced with a statically typed language with generics and higher order functions.
On the other hand, I have no idea why a compose function that takes exactly 6 arguments, the last of which is function which takes 3 arguments would be a desirable abstraction. But I don't think static typing is necessarily to blame for this -- this just looks like a clunky function that has a clunky type.
> I think that this signature is pretty clear for anyone experienced with a statically typed language with generics and higher order functions.
Sounds like I have stuff to study!
Maybe ramda was an extreme example (with or without typescript, it was so hard to read that our dev team decided to just remove it altogether and replace it with more verbose but easier to read vanillaJS code or equivalent lodash functions). But I come across difficult TypeScript examples nearly every day of my work, where I feel like I'm reading an obfuscated leetcode challenge instead of the straightforward business logic in the rest of the codebase.
Once I finally understand a complex type, my usual reaction is, "That's it? That's all that was trying to express?" It's just an arcane syntax to me. Sounds like learning about generics and higher-order functions in statically typed languages would be a good starting place... thanks!
From what you are writing, you really lack the basics here. Learning on the job is fine, but sometimes it's worth to spent dedicated time to learn foundations or at least get someone on board who can teach them.
I suggest to try to get your boss to sponsor this, since you need it for the job. It will also make your dev experience so much more fun!
For sure. I am no TypeScript expert, and for the most part I don't really need to be... most of our types are pretty straightforward, arrays of strings and such.
Learning is great, but this job, like many others, doesn't really have a well-defined system for training, documentation, professional development, or anything like that. Either I learn on the job as it happens or I don't learn at all. There's always too much to build, with constantly shifting priorities defined and redefined by higher-ups who don't know or care what TypeScript is. Sure, I can push back on that to some degree and beg for a resource or two, but even that is difficult, and there's always so much else that's even more pressing to learn. And web dev by its nature is a broad and shallow career anyway, especially on the frontend... by the time you begin to master something, it's already obsolete lol.
TypeScript looks like it'll have some staying power, so I'm happy to learn it as I go. But over time, I've learned to stop chasing perfection and to just go for "Will this survive long enough until the rewrite in a year or two? If so, good enough...". I've never known a job like this where code survives longer than 2 or 3 years before someone, either a dev or a manager, wants to rewrite it from scratch.
A lot of our existing codebase was written by contractors who had a lot of experience, but little desire to document anything or comment anywhere. Our current generation of (relatively junior) devs inherited that, has trouble with a lot of it, and ends up rewriting large overengineered swaths in simpler patterns as we go. A complete rewrite is already planned. And so the cycle continues :)
In general, the barrier to entry for JS/web dev is pretty low, and so there are a lot of low-to-med skilled devs like me in the industry. I think, philosophically, I lean against writing code that is overly "clever" rather than readable. Similarly with types. If a typing becomes complex enough that it's not really readable, I'd rather just leave a clear comment as to what the intent is and then move on, knowing that the code itself -- much less its typing -- is unlikely to survive long anyway. At the end of the day, IMO, it doesn't make sense to have types that are more complicated than the code itself... if correctness is important but the typing is complicated, I try to break down the code itself, add comments, add unit tests, add documentation, etc. rather than try to coerce TypeScript into sentience.
Is that the most "correct" way? Probably not. But it sure makes things easier to read in PRs rather than telling everyone, "Well, you need to learn advanced TypeScript if you want to read my contribs."
I think I see where you come from. And for libraries and even frameworks I would agree.
But typescripts advanced features are more like advanced SQL. Sure, you'll learn some chunk specific to your database, but the majority will be transferable. And just like SQL it won't become outdated knowledge in the next decades most likely.
So I still think it will be worth. If not for the company then at least for yourself. :)
Well, you are likely taking examples from library internals.
Libraries exist, in part, to encapsulate high complexity.
There's likely accompanying documentation for the examples you provided.
In some other languages you have similar stuff, with the added complexity of concurrency and memory management related types. If you are struggling with TypeScript, let me tell you about a whole new world of pain called C++.
That's why I think every programmer should learn C++.
I've been professionally employed as a software developer since 1998 and I am currently curled up in a ball in the corner rocking slowly back and forth after seeing that.
Personally, I think the equivalent code would look bad in most languages, even with more verbose argument/type names and comments (which people seem to overlook often) - it's just that we try to make our applications do a bit too much.
Just look at some of other examples in the sibling comments!
Another random example from Axios: <T = V>(onFulfilled?: (value: V) => T | Promise<T>, onRejected?: (error: any) => any): number;
Or eslint: type Prepend<Tuple extends any[], Addend> = ((_: Addend, ..._1: Tuple) => any) extends (..._: infer Result) => any ? Result : never;
Here's another real example from today... I was trying to figure out how to type "the name of this type's key has to be one of the following strings in this enum, but the type doesn't need to have all the keys". Here's a Stack link with the right answer: https://stackoverflow.com/a/59213781, but it wasn't easy to figure out. At first I thought it would be `[key in Partial<MyEnum>]`, but nope. Maybe optional? `[key in MyEnum]?` kind of works but fails in an new way (see the Stack for details). The correct way to do it is apparently `Partial<Record<MyEnum, unknown>>`, which I NEVER would have been able to figure out. Why the record? Why the unknown? Who knows..?
Don't get me wrong, I love TypeScript for the simpler use cases, and a lot of it IS that, thankfully. But the more complex compositions, especially in popular third-party libs? I've given up lol.
The use of single-letter keywords (K, T, V, P, R, etc.) combined with confusing re-use of punctuation (<> and : and () and []) that mean subtly different things depending on where they're used, on top of how JS already uses them, makes it even more so. Sometimes I wish TypeScript were more verbose and opted for longer, clearer constructs rather than stacked shorthands...