Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I generally do this via a `throw UnsupportedValueError(value)`, where the exception constructor only accepts a `never`. That way I have both a compile time check as well as an error at runtime, if anything weird happens and there's an unexpected value.


The fact that there can be runtime type errors that were proven impossible at compile time is why I will never enjoy TypeScript.


TypeScript isn't primarily meant to be enjoyed.

It is meant to be a much better alternative to Javascript while dealing with the fact that the underlying engines use and existing programmers were used to Javascript.

That said I absolutely enjoy TypeScript, but that might be because I suffered from having to deal with Javascript from 2006 until TypeScript became available.


I have the exact same reason I enjoy typescript - raw dogging js before was an absolute nightmare of testing every single function for every possible value that might be thrown in and being able to handle shit data everywhere.

god-awful code.


As a C# dev, backend typescript is fantastic and the type system is light years ahead of C# in expressivity.

But the learning curve... no shit.


If Typescript is javascript with types bolted on, Rescript is javascript with types the way it should have been. Sound types with low complexity. https://rescript-lang.org/


That syntax is so alien to me as a JS/TS developer. I mean coffeescript was as well until JS slowly introduced it all.

It would be cruel for me to force ReScript onto the team because they'd all need to reskill. I could only use it for a private project and then hire exclusively for it afterwards


Really, that is surprising to hear. There are a couple of differences but most of the syntax looks the same to me, what part do you find alien?

The reskill problem is of similiar difficulty with learning a new framework I think. Especially because the language is rather simple compared to typescript (which is also its strength).

I do understand it is an uphill battle. The whole nobody get's fired for choosing IBM thing. The language is still unproven in the general perception. I do think that when it comes to libraries and frameworks I see a lot of developers choose new unproven stuff, more then they do languages.


Does this have a relation to Reason/Reason ML?



Agree wholeheartedly.

Writing TypeScript is better than JavaScript, but the lack of runtime protection is fairly problematic.

However, there are libraries such as https://zod.dev, and you can adopt patterns for your interfaces and there's already a large community that does this.


Zod is quite unpleasant to use, IME, an has some edge cases where you lose code comments.

From experience, we end up with a mix of both Zod and types and sometimes types that need to be converted to Zod. It's all quite verbose and janky.

I quite like the approach of Typia (uses build-time inline of JavaScript), but it's not compatible with all build chains and questions abound on its viability post Go refactor.


> we end up with a mix of both Zod and types and sometimes types that need to be converted to Zod

In my code, everything is a Zod schema and we infer interfaces or types from the schemas. Is there a place where this breaks down?


Not that I know of aside from code comments (which I like), but I much prefer writing TypeScript to Zod


Could you please elaborate on "patterns for your interfaces"?


Sure. You tend to think about the edges of your application.

1. Router

Tanstack Router: Supports runtime validation libraries such as z0d. So I have routes such as example.com/viewer/$uuid/$number, it should 400 if those aren't actually validate uuid and numbers.

React Router: Supports Types, but every type is a string because, well, they technically are, but this isn't useful in practice in my opinion. There are 3rd party libs such as: https://github.com/fenok/react-router-typesafe-routes

2. API

Lets say you're making your API public to clients you can't trust to send the correct data ( which probably also includes your own client ).

https://www.npmjs.com/package/express-openapi-validator

This library advertises validating both your input and your output

3. State

https://github.com/pmndrs/zustand/discussions/1722

4. Database

https://www.npmjs.com/package/prisma-zod-generator

5. Forms

https://medium.com/@toukir.ahamed.pigeon/react-hook-form-wit...

6. ENV

https://jfranciscosousa.com/blog/validating-environment-vari...

Obviously checks on the agent are primarily a DX/UX thing, whilst checks on the server step are also security controls.


Isn't that not necessarily out of the ordinary though? What if there's a cosmic ray that change's the value to something not expected by the exhaustive switch? Or more likely, what if an update to a dynamic library adds another value to that enum (or whatever)? What some languages do is add an implicit default case. It's what Java does, at least: https://openjdk.org/jeps/361


> What if there's a cosmic ray that change's the value to something not expected by the exhaustive switch?

I could forgive that.

The TypeScript case is more like "what if instead of checking the types we just actually don't check the types?".


This is how all static type checking works. What programming language do you have in mind that does static type checking and then also does the same type checking at runtime? And what would you expect this programming language to do at runtime if it finds an unexpected type?


I think the point is that other languages make guarantees that ensure you don't have to do any runtime checking. In TypeScript, it's far too easy (and sometimes inevitable) to override the type checker, so some poor function further down into the codebase might get a string when it expects an object, even though there are no type errors.


Compiler exhaustion is such a useful feature. I can’t believe TS doesn’t have it.


> The fact that there can be runtime type errors that were proven impossible at compile time is why I will never enjoy TypeScript.

The "impossibility" is just a trait of the type definitions and assertions that developers specify. You don't need to use TypeScript to understand that impossibilities written in by developers can and often are very possible.


My first introduction to TypeScript was trying to use it to solve Advent of Code.

I wrote some code that iterated over lines in a file or something and passed them to a function that took an argument with a numeric type.

I thought this would be a great test to show the benefits of TypeScript over plain JavaScript: either it would fail to compile, or the strings would become numbers.

What actually happened was it compiled perfectly fine, but the "numeric" input to my function contained a string!

I found that to be a gross violation of trust and have never recovered from it.

EDIT: See https://news.ycombinator.com/item?id=46021640 for examples.


> I thought this would be a great test to show the benefits of TypeScript over plain JavaScript: either it would fail to compile, or the strings would become numbers.

You're just stressing that you had a fundamental misunderstanding of the language you were using when you first started to use it. This is not a problem with the language. You simply did not understood what you were doing.

For starters, TypeScript does not change the code you write, it only helps you attach type information to the code you write. The type information you add is there to help the IDE flag potential issues with your code. That's it.

See how node introduced support for running TypeScript code: it strips out type info, and runs the resulting JavaScript code. That's it.

If your code was failing to tell you that you were passing strings where you expected numbers, that was a bug in your code. It's a logic error, and a type definition error.

Static code analysis doesn't change the way you write code. That's your responsibility. Static code analysis adds visibility to the problems you're creating for yourself.


No tool is perfect. What matters is if a tool is useful. I've found TypeScript to be incredibly useful. Is it possible to construct code that leads to runtime type errors? Yes. Does it go a long way towards reducing runtime type errors? Also yes.


> No tool is perfect. What matters is if a tool is useful

Some tools are more perfect and more useful than others.

Typescript's type system is very powerful, but without strict compile-time enforcement you still spend a lot of effort on validating runtime weirdness (that the compiler ought to be able to enforce).


Yes that's true, but there's effort to consider on both sides of design decisions like those TypeScript has made. Much of the compile time behaviour comes from the decision for TypeScript to be incremental on top of JavaScript. That allows you to start getting the benefit of TS without the effort of having to rewrite your entire codebase, for example. Having used TS for many years now I feel that the balance it strikes is incredibly productive. Maybe for other folks/projects the tradeoff is different - but for me I would hate going back to plain JS, and there's no alternative available with such tight integration with the rest of the web ecosystem.


Have you seen ReScript? Of course it is not as popular as typescript but it improves on all the bad parts of typescript. You'll get sound types with the pain points of javascript stripped out. Because it compiles to readable javascript you are still in the npm ecosystem.

You don't have to rewrite your whole codebase to start using it. It grows horizontally (you add typed files along the way) compared to typescript which grows vertically (you enable it with Any types).

The point is that we don't have to move back to plain js. We have learned a lot since typescript was created and I think the time has come to slowly move to a better language (and ReScript feels the most like Javascript in that regard).


> Typescript's type system is very powerful, but without strict compile-time enforcement you still spend a lot of effort on validating runtime weirdness (that the compiler ought to be able to enforce).

That's something that you own and control, though. Just because TypeScript allows developers to gently onboard static type checking by disabling or watering down checks, that does not mean TypeScipt is the reason you spend time validating your own bugs.


> Just because TypeScript allows developers... does not mean TypeScipt is the reason you spend time validating your own bugs

Unfortunately, taking an ecosystem-wide view, it means exactly that. If one of my dependencies hasn't provided type stubs, or has provided stubs, but then violated their own type signatures in some way, I'm on the hook for the outputs not matching the type annotations.

In a strict language, The compiler would assert that the dependency's declared types matched their code, and I'd only be on the hook for type violations in my own code.


It's way better than having to write untyped JavaScript though


TypeScript is neither sound nor complete and was defined that way, beating out other competitors that were sound and/or complete.

What I mean to say is - TypeScript isn't proof.


That scenario is usually either misuse of escape hatches (especially at API boundaries) or a misunderstanding of what Typescript actually guarantees.


Not really, I provided these examples a couple weeks ago on another HN thread. TypeScript is simply unsound.

https://www.typescriptlang.org/play/?#code/MYewdgzgLgBAllApg...

https://www.typescriptlang.org/play/?#code/DYUwLgBAHgXBB2BXA...


Aren't these bugs that could be "simply" reported and fixed? Or maybe those would get a label "not a bug" attached by the TS creators for some reason?


Both are by design. Array covariance is a common design mistake in OOP languages, which the designer of TypeScript had already done for C# but there they at least check it at runtime. And the latter was declared not-a-bug already IIRC.

TypeScript designers insist they're ok with it being unsound even on the strictest settings. Which I'd be ok with if the remaining type errors were detected at runtime, but they also insist they don't want the type system to add any runtime semantics.


"By design", for me, doesn't say that it can't be changed — maybe the design was wrong, after all. Would it be a major hurdle or create some problems if fixed today?


Perfect examples of the kind of thing I'm talking about, thank you.


In the first example you deliberately create an ambiguous type, when you already know that it's not. You told the compiler you know more than it does. The second is a delegate, that will be triggered at any point during runtime. How can the compiler know what x will be?


First example: you're confusing the annotation for a cast, but it isn't; it won't work the other way around. What you're seeing there is array covariance, an unsound (i.e. broken) subtyping rule for mutable arrays. C# has it too but they've got the decency to check it at runtime.

Second example: that's the point. If the compiler can't prove that x will be initalised before the call it should reject the code until you make it x: number|undefined, to force the closure to handle the undefined case.


For the first one, the compiler should not allow the mutable list to be assigned to a more broadly typed mutable list. This is a compile error in kotlin, for example

    val items: MutableList<Int> = mutableListOf(3)
    val brokenItems: MutableList<Any> = items


> The second is a delegate, that will be triggered at any point during runtime. How can the compiler know what x will be?

x is clearly defined to be a number. The compiler should produce an error if the delegate captures x before it has a value assigned.


If it only works when you write the types correctly with no mistakes, what's the point? I thought the point of all this strong typing stuff was to detect mistakes.


Because adding types adds constraints across the codebase that detect a broader set of mistakes. It's like saying what's the point of putting seatbelts into a car if they only work when you're wearing them - yes you can use them wrong (perhaps even unknowingly), but the overall benefit is much greater. On balance I find that TypeScript gives me huge benefit.


Same here, you can also use the same function in switch cases in Angular templates for the same purpose. Had no idea you could achieve similar with `satisfies`, cool trick.


That's great, I'm going to use that one in the future.


That's very clever!


We have this nifty util in our codebase:

```ts

/*

* A function that asserts that a value is never.

* Useful for exhaustiveness checks in switch statements.

*/

export function assertNever(x: never): never {

  // eslint-disable-next-line @typescript-eslint/restrict-template-expressions

  throw new Error(`Unexpected object: ${x}`)
}

```




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: