Hacker News new | ask | show | jobs
by nsxwolf 1373 days ago
I currently have to occasionally contribute to a TypeScript codebase at work. I appreciate how much better it is than Javascript. When I write code as an outsider (Java and Go developer), I feel like I use the type system in sensible and readable ways. When I look at the code written by the native TypeScript experts in my company it is a bewildering, abstract, unreadable morass. I have no idea what's going on half the time.
1 comments

Yeah, what a terrible syntax :(

Just today I was looking at the type definition for a third-party lib (ramda)... what the heck does this even mean...

compose<V0, V1, V2, T1, T2, T3, T4, T5, T6>(fn5: (x: T5) => T6, fn4: (x: T4) => T5, fn3: (x: T3) => T4, fn2: (x: T2) => T3, fn1: (x: T1) => T2, fn0: (x0: V0, x1: V1, x2: V2) => T1): (x0: V0, x1: V1, x2: V2) => T6;

Got it?

compose is a higher order function. In the first step it accepts a function that converts 3 values (V1 to V3) into a single values (T1) and a series of conversion functions that converts this single value into another value (T1 into T2, T2 into T3 and so on until T6). Using these functions it produces a new function that converts the combination of V1, V2 and V3 into a T6.

I don't know ramda, but I assume this is only part of the type definition of compose and this is just the longest part of it. I think compose is written in such a way that it can accept a many conversion functions as you want and this is just the longest variant that is encoded in the types.

Yes, that's correct... compose() pipes a bunch of passed functions from right to left. Here's the actual type definition if you want to see: https://github.com/googol/DefinitelyTyped/blob/6836f798cb186...

But even at its simplest variant, with just one or two functions passed, what the V0 or T1 do is pretty confusing. I thiiiiiiiiink it's trying to ensure the return type of one function is correctly passed as the input type of the subsequent function, and so on and so forth, but I don't really know.

Also, I should note that I'm using an older version of that lib. The latest version has cleaner typings... still difficult, but at least formatted better: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/mast...

It indeed ensures one a type level that this series of functions can work as a series so every subsequent function can accept the return type of the previous one.

The latest version seems to have abandoned the typing of the initial input, which makes the types a little simpler.

My point however is that the types you point our here are actually not particularly complex. They are just long, with lot's of inputs for the generics (and the syntax may be confusing). The types in the original article are much more complicated, with conditional type inference etc.

Great and simple explanation. Suddenly it doesn't feel cryptic, simply logical. Thx!
If expressed in OCaml, it's just

let compose f5 f4 f3 f2 f1 f0 x0 x1 x2 = f5 (f4 (f3 (f2 (f1 (f0 x0 x1 x2)))))

whose signature is,

val compose : ('a -> 'b) -> ('c -> 'a) -> ('d -> 'c) -> ('e -> 'd) -> ('f -> 'e) -> ('g -> 'h -> 'i -> 'f) -> 'g -> 'h -> 'i -> 'b = <fun>

Funny thing: what you are complaining about here is in fact a lack of power in typescripts typesystem.

And even that verbose signature is better than none imo.

Imho it’s kind of pointless to judge how simple a type definition looks like. The definition is there to make sure the interface is correctly typed. Edit: and sometimes a simple/beautiful interface requires complex types..
When I'm using someone else's code, though, I need to be able to understand what it's expecting and returning... isn't that the point of a typing system? It's not just an internal unit test, but a signal to other developers of how the function is supposed to work (especially if it's exported and intended for reuse).

Quite often I get a function that's working correctly but typed incorrectly (including in someone else's typescript definitions), and sometimes I can correct them but other times I can't even read the original author's intent...

And edit: It's not that types have to be simple, but that complex types (especially) should be readable, as in you can follow the complexity step by step, line by line.

I feel like that definition is the TypeScript equivalent of "callback hell" or similar. It almost looks minified or obfuscated, or just written to be super terse instead of clear. I don't really know which it is, because I can't even begin to read it...

I'm not a TypeScript expert by any stretch, but I've been using it 40 hours a week for the last year and I SHOULD at least be able to start to read it... but nope. And I come across examples like that multiple times a week. It's just a really bizarre syntax, unlike any of the other languages I've ever used. I think it's like that because they had to hack it on top of Javascript, vs a language being strongly-typed from the getgo.

> I think it's like that because they had to hack it on top of Javascript

This is the source of most of typescript's flaws, but the mediocre type syntax was a deliberate choice: it's all erased at compile time, so javascript imposes no constraints. My guess is that it's just because many of the original typescript devs were on the C# language team.

It's very simple to understand. You don't have to actually read the definition just know what "compose" does at a high level.
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'm fully with you on the second example though.

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

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

Single letter variables are bad when they're values, and bad when they're types.

Say no to single-letter variables!

That compose thing likely came from a library not application code.

It also must have documentation other than the type annotation.

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!