Remix.run Logo
amake 10 hours ago

99% of my use of `satisfies` is to type-check exhaustivity in `switch` statements:

    type Foo = 'foo' | 'bar';
    
    const myFoo: Foo = 'foo';
    switch (myFoo) {
      case 'foo':
        // do stuff
        break;
      default:
        myFoo satisfies never; // Error here because 'bar' not handled
    }
mckirk 8 hours ago | parent | next [-]

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.

jstanley 4 hours ago | parent | next [-]

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

thomasikzelf 2 hours ago | parent | next [-]

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/

ownagefool 2 hours ago | parent | prev | next [-]

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.

eitland 2 hours ago | parent | prev | next [-]

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.

virtue3 an hour ago | parent [-]

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.

Defletter 3 hours ago | parent | prev | next [-]

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

jstanley 2 hours ago | parent [-]

> 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?".

jaapz 2 hours ago | parent | prev | next [-]

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

locknitpicker 3 hours ago | parent | prev | next [-]

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

jstanley 3 hours ago | parent [-]

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.

macguillicuddy 14 minutes ago | parent [-]

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.

triyambakam 3 hours ago | parent | prev [-]

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

debugnik 3 hours ago | parent [-]

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

jstanley 2 hours ago | parent | next [-]

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

LordN00b an hour ago | parent | prev [-]

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?

bspammer an hour ago | parent | next [-]

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
debugnik an hour ago | parent | prev | next [-]

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.

jstanley an hour ago | parent | prev [-]

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.

macguillicuddy 6 minutes ago | parent [-]

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.

Klaster_1 3 hours ago | parent | prev | next [-]

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.

mquander 7 hours ago | parent | prev | next [-]

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

rezonant 7 hours ago | parent | prev [-]

That's very clever!

your_fin 3 hours ago | parent | prev | next [-]

I would highly recommend the ts-pattern [1] library if you find yourself wanting exhaustive switch statements! The syntax is a bit noiser than case statements in simple cases, but I find it less awkward for exhaustive pattern matching and much harder to shoot yourself in the foot with. Once you get familiar with it, it can trim down a /lot/ of more complicated logic too.

It also makes match expressions an expression rather than a statement, so it can replace awkward terenaries. And it has no transitive dependencies!

[1]: https://github.com/gvergnaud/ts-pattern

ervine 10 hours ago | parent | prev | next [-]

https://typescript-eslint.io/rules/switch-exhaustiveness-che... if that is something you're not aware of!

inlined 10 hours ago | parent | prev | next [-]

Nice. I didn’t know I can now replace my “assertExhaustive” function.

Previously you could define a function that accepted never and throws. It tells the compiler that you expect the code path to be exhaustive and fixes any return value expected errors. If the type is changed so that it’s no longer exhaustive it will fail to compile and (still better than satisfies) if an invalid value is passed at runtime it will throw.

preommr 10 hours ago | parent [-]

I thought the same thing. I also have an assert function I pull in everywhere, and this trick seemed like it would be cleaner (especially for one-off scripts to reduce deps).

But unfortunately, using a default clause creates a branching condition that then treats the entire switch block as non-exhaustive, even though it is technically exhaustive over the switch target. It still requires something like throwing an exception, which at that point you might as well do 'const x: never = myFoo'.

klinch 2 hours ago | parent | prev [-]

TIL.