I love this thread, it has two of my favorite HN topics:
1) People shitting on JavaScript not realizing that their "obviously better" solution was considered and found not a good solution.
2) People shitting on TypeScript not realizing that conditional types and template literal types are awesome. I really like those type-safe routers (https://tanstack.com/router/v1/docs/guide/type-safety) and fully-typed database clients (https://www.edgedb.com/docs/clients/js/index#the-query-build...).
Not a rebuttal, just curious, how do you find your productivity compares to a "real" statically typed language (e.g. Go, Java, etc.)? Does your increase in productivity compared to JavaScript simply come from static typing, or is TypeScript itself the source?
Both. Static typing makes JavaScript well more than tolerable. Dare I say enjoyable! But I also love TS' duck typing and I love how it's totally optional.
The optionality is a special trait that most "real" static languages don't get to enjoy (for better or worse). By being optional, I can simply ignore types when I'm hacking/prototyping something and type safety/soundness is the last thing I care about. Rust, for example, can feel really tedious when it makes me "show my work" and I'm just trying to scribble something up quickly. If that makes sense? I just make `any` a linting error so I can use it during my experimentation/prototyping but I cannot ship it.
Typescript has quite a few pitfalls like the uselessness of Readonly<T>, which can just vanish with an assignment to a variable typed T where TS will neither warn nor error out nor require an explicit cast.
I agree. I feel like sketching in JS and solidifying in TS is super productive. Being able to go from a plain old JS object to a type is really satisfying. I feel like full stack TS might be a great choice for new web projects. And if you find a bottleneck, you could rewrite in Rust.
> And if you find a bottleneck, you could rewrite in Rust.
Sounds pretty extreme..? What kind of bottlenecks would you be talking about that would prompt me to re-write my entire application in another language?
Not GP, and not a direct answer to the question… in these discussions a lot of the “benefit of static types” gets boiled down to:
- documentation
- aids tooling (eg improved static references in editor, static analysis by linter, etc)
- catches a whole class of problems before runtime (eg a typecheck-time substitute for certain runtime checks and their corresponding tests)
I often like to add:
- promotes better design
This puts TypeScript’s type system in a unique position, where it’s simultaneously expressive and complex in ways many others are not (because it’s designed to express extraordinarily dynamic idioms used in JS) and equally mundane and simple if you use it from the start (because all that complexity is a good forcing function for deciding whether you all of that dynamism it can express is of value for the thing you’re actually building).
In a more general sense, it strongly encourages developers to think about their interfaces explicitly where they might otherwise “rapidly prototype” a monstrous one.
The vast majority of the time, TypeScript will encourage simpler APIs that don’t need most of TypeScript’s “advanced” type system features. And in terms of productivity, that’s a benefit you only get if you’re starting from the proverbial Dynamic Wild West. So it might not be absolutely more productive than other statically typed languages, but still relatively an enormous productivity boost in its context.
Not OP but a well-typed codebase means you have to remember less, your broken code gets caught before you even run/compile it, and you can just lean on the type system to write the code for you.
And TypeScript has a much more expressive type system compared to Go, Java, and frankly most languages. Java in particular still has pretty bad type reification (where ArrayList<Integer> and ArrayList<Float> are essentially the same and cannot be used to overload methods, for example).
I use a lot of the utility TypeScript functions (i.e. Partial<>, Record<>) and it catches all sorts of mistakes that might be made by myself in the future or someone not familiar with the codebase. For example, if there has to be a A->B mapping somewhere, I would type B so it would raise a type error if something was added in A but not B. Most type systems are not flexible enough to do anything remotely like that and instead you have to write a bunch of tests where you end up 20 lines of very basic test code for 1 actual line of code.
I have a bit of experience with TypeScript and I think that its types are pretty good at catching mistakes. You need to run linter (like eslint) and some strict compiler options to take a full advantage of it, but that's not a big deal. And TypeScript type system is miles ahead of Java. I'm not sure if those types provide any productivity increase compared to Java, that's questionable, but at least it's not JavaScript and that's good enough for me. Now JavaScript is truly atrocious hit to productivity. I just can't stand it.
My only issue with TypeScript is its strange design with regard to interface and types. Those are pretty fundamental concepts and they're absolutely similar. I don't understand this design and I think that only one thing should have left, but may be I'm wrong about it. Also I think that TypeScript documentation would benefit from more examples with hard concepts, I didn't fully understand its advanced generics concepts. But that's not a big thing and probably more on me.
> My only issue with TypeScript is its strange design with regard to interface and types. Those are pretty fundamental concepts and they're absolutely similar. I don't understand this design and I think that only one thing should have left, but may be I'm wrong about it.
I feel the same way. IIRC, earlier versions of Typescript were much more clearly influenced by .NET and other Microsoft idioms, and there were a few features that didn't seem to make much sense coming from the Javascript side of things. I also had a buddy of mine that's a Java developer be very confused about how they were supposed to work.
> Also I think that TypeScript documentation would benefit from more examples with hard concepts, I didn't fully understand its advanced generics concepts. But that's not a big thing and probably more on me.
Absolutely agree on this, although things are getting better. For awhile it seemed like the only documentation for more advanced features was the main website's blog posts and release notes.
One major downside of TypeScript is that types are not available at runtime and that computations on types can't be done by writing regular TypeScript (both possible with code generators of course).
Types are absolutely available at runtime, you just have to start from the runtime. Type guards are the solution everyone wants +- a tiny bit of ceremony, and adding type system information to the runtime would just hurt performance for no good reason.
This:
type Foo = {
bar: string
// …
}
Can just as easily be addressed as:
const parseFoo = parse.object({
bar: parse.string,
// …
})
type Foo = Foo<typeof parseFoo>
(Where parse methods here are type guards for their respective parsed return types, and can similarly be used to serialize runtime values as needed.)
For that little bit of extra ceremony, you get to isolate runtime stuff to the boundaries you need to care about, and let the type checker deal with everything else (typically most everything internal).
Have you ever used a language where types can influence runtime behavior? It's an extremely powerful tool to do things like choosing a specialized sort algorithm based on the type of contents of array you are sorting. This has zero overhead iff types are known at compile time and can yet hit different code paths at runtime. To just dismiss this as not needed is extremely short sighted in my opinion.
I have. And I think the limitation is good for a language that doesn’t have that. I don’t want TypeScript to lie to me about its capabilities, I want it to help me make good decisions with the runtime code I actually have.
TypeScript types are not available at runtime. As the name implies, runtime type information requires runtime support. Since TypeScript compiles to JavaScript, the only type information available is that provided by JavaScript.
Types are absolutely available at runtime. You just have to start with the runtime.
const parseStr = (value: unknown): assert value is string => {
if (typeof value === 'string') return value
throw new Error(…)
}
const str = parseStr(anythingYouCanThrowAtIt)
I can 100% guarantee you str is going to have the same static and runtime type once you’ve checked it, or parsed it from whatever type you’d accept as a string. TypeScript won’t do the parsing for you, because JavaScript doesn’t have clear semantics for runtime casting that anyone wants or would accept. But type guards are exactly the solution to that and incredibly composable. Once you accept that reality and embrace it, getting the static type out of your runtime parser is a single added line of static type code. And all of this is almost exactly equivalent to what languages with runtime casts do, but you have complete visibility into it because you determine how casts behave. There are whole libraries which do this for you so don’t worry about rolling your own unless you have very particular needs. But TypeScript definitely has the facility to align static and runtime types however you see fit. You just need to tell the type system what types the runtime conveys, same as every other aspect of the type system.
You seem knowledgeable about TypeScript, so you certainly very well understand what people mean by "types are not available at runtime". Once your typescript is compiled to javascript, there's nothing left of your user-defined types and interfaces. Type guards are a security that is very much a workaround for the lack of types at runtime.
I’m encouraging thinking about the problem from a different angle. If you parse rather than validate[0][1], you have roughly all of the type information you could want in the runtime, and by using type guards the type system will reflect the runtime types.
It’s not a workaround. Every language with runtime types will have some logic devoted to this kind of casting/narrowing. TypeScript rightly doesn’t build it into the compiler because most of the time you don’t want types to have runtime behavior. For internal logic which can be validated statically, runtime types would be an unnecessary overhead. So it’s up to developers to determine where it should be applied. Type guards are explicitly an affordance for that, designed specifically to convey types with runtime casting/narrowing.
If you use good, composable primitives like zod or io-ts or any of several other implementations, your code will typically be almost identical to deriving runtime from types rather than the inverse.
> I can't do in TS with the help of a library or two.
Libraries like io-ts and zod embed the type information into JavaScript, so the information is available at runtime but only for wrapped types. If TypeScript provided runtime type information, such libraries wouldn't be necessary.
Can only speak for java. But I'm way more productive with typescript. Typescript has 2 things going for it:
- type system is way more flexible. You can create, merge, pick keys and manipulate types in all kinds of ways. You have better type inference, type conditionals and lots more
-there's an escape hatch. Sometimes you just want to quickly try something, or you're under time pressure and need to either break the types or do a bigger rewrite, or you just don't care about a certain variable's type. Then there's the any/unknown/plain js/@ts-expect-error escape hatches to do just that.
It's a bit of both for me. I'd say 90% of it is from simply having types; 10% is from some of the cool things you can do with TS types that aren't common in most statically typed languages.
For me (coming from C++) being able to easily union and intersect types, and being able to specify certain values (not sure the right name for this). For example:
type ServerResponse = {success:true, response:JSON} | {success:false, error:string};
In C++ I would probably have to type success:boolean, and make response and error optionals, but the TS type is more expressive.
if success==true, then response must exist.
if success==false, then error must exist.
TS understands this type really well, and type narrowing makes it easy to work with.
// NOT LEGAL; error might not exist
console.log(sr.error);
// IS LEGAL; we test for success first, which narrows the type.
if(sr.success){
console.log(sr.response);
} else {
console.log(sr.error);
}
Too late to EDIT: but "literal types" was the word I am looking for.
I used "success:true" and "success:false" as part of the two types I was combining; it seems like "success:boolean" would server the same function, but it does not. The type I created has more information that that.
I'm a big fan of TS, but discriminated unions are available in a lot of languages and when not, there's usually some library adding it like boost::variant if the language has runtime types or templating/macros.
Some folks have built whole SQL databases and DSL compilers in the TS type system. These tend to be toy projects with disclaimers not to use them. But the type system being Turing complete[0] (for better or worse), pretty much whatever you can imagine. This project[1] is one I actually return to frequently for practical ideas.
- easy referencing the type of another struct's field. As in `function parseInput(input: SomeType['someField']) { }`
- it's ability to infer types in general
- parsing. given a route string like `/users/:userId` TypeScript can force you to always pass `{ userId: 1234 }` when actually building the route without having to declare the interface anywhere
I think the biggest benefit I've found is TypeScript's inference is extremely smart and in fact it's considered a best practice to basically write as little TypeScript as possible and rely as much as possible on TS's inference
In general, if a "best practice" of a language is to write less code that tends to translate to great productivity
> Does your increase in productivity compared to JavaScript simply come from static typing, or is TypeScript itself the source?
As far as I can tell, TypeScript doesn't add any new features or behavior to JS other than static typing.
If you ask me, of course TS isn't as good as the real deal, but it's darn close and way better than vanilla JS or compilers for other languages that target JS.
I really dislike how unpredictable the typescript compiler can be. What I mean is, there isn’t a simple set of rules I can understand to know when annotations will be required. Most of the time, this doesn’t matter, but occasionally it blows up and development comes to a screeching halt. Contrast this to, say, Java and ML type-systems, which are highly predictable.
I think the opposite, TS is much more predictable than hindley-milner inference, mostly because the inference only flows forward.
Yes, there are a couple of weird edge cases with arrow functions in generics, and it gets a bit more complicated with narrowing, but aside from that this never a problem I've run into. IMO the verbose error messages for complex types are a much bigger problem, which could be solved with better UIs for error messages (syntax highlighting and code folding).
The rule I tend to follow is "annotate as little as possible". This has worked out great for me so far since TypeScript's inference gets it right 90% of the time and it leads to more maintainable code.
Also after coding 5 days a week for a full year with it I really can't relate to the idea that it is somehow unpredictable. I think, like most things, you eventually get a "feel" for it and don't even have to think about it.
That's what I love most about it. It feels like it takes up no extra mental real estate for me. It's excellent at never "getting in the way" (when used properly)
Use LSP feature that reveals what TS thinks is the type of the identifier under the cursor (I have it bound to Shift+K in Neovim). If not right, annotate (or better go back and type properly whatever means you obtained that value).
TypeScript flags when I call a function that takes two parameters max with three arguments as an issue in many contexts, but not in contexts like this.
I understand why they chose to do this, but I reach for better type systems any time I have the choice.
On the other hand, Stockholm Syndrome is an underrated and under-researched effect in software engineering.
There are people ready to defend with their life the most obtuse, inane piece of technology, just because they have invested a non-negligible amount of time getting used to its quirks, that they become completely blind to its faults, and start to believe those very quirks are actually positive and to be cherished.
> TanStack Router is built to be extremely type-safe.
This is alludes to my only major complaint about typescript. IMO, something is either type-safe or it ain't. I don't like how the JS bleeds through if you aren't careful.
I'm not a TS hater by any means, I use it regularly and am usually pleased. I just can't help but compare it to the [S,Oca]ML compilers I'm so fond of.
There is so much wrong with JS, the only reason it is used is because people are literally forced to.
It took a long time for js devs to accept the notion of a compiler with TS. While TS is an improvement, it’s only a localized one. No chance ever, any other language could have replaced it.
> There is so much wrong with JS, the only reason it is used is because people are literally forced to.
There are perhaps at least a hundred (when not hundreds of) compile-to-js languages, yet people still use it. Even in back-end!
> It took a long time for js devs to accept the notion of a compiler
People who write programs in JS cannot be so easily grouped together, I think. There's this eternal September of bootcamp devs writing... interesting... code as we all did when we first started, but I personally went first with C, Perl, then PHP, then JS, then .NET, then Python and now continue mostly in TS and Java. Among them TS is the one I like the most (and the ones starting with P are the ones I dislike the most - not that it matters, use whatever works for you).
Have you used EdgeDB in prod? I’m itching to try it, but am too worried about general instability. Seems like too many moving parts. Love the DX though, it’s really clean and well thought out.