top | item 46019613

(no title)

amake | 3 months 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
    }

discuss

order

mckirk|3 months ago

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|3 months ago

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

Klaster_1|3 months ago

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|3 months ago

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

rezonant|3 months ago

That's very clever!

your_fin|3 months ago

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

inlined|3 months ago

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|3 months ago

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

nikeee|3 months ago

I still keep my assertNever function because it will handle non-exhaustiveness at runtime.

Normal_gaussian|3 months ago

This is what I do:

   class AbsurdError extends Error {
     constructor(public value: unknown, message: string) {
       super(message);
       this.name = 'AbsurdError';
     }
   }
   
   function absurd(value: never, message: string) {
     throw new AbsurdError(value, message);
   }
Including an error message and an error type helps if one does slip through to runtime. Additionally, the AbsurdError can be caught and escalated appropriately. And finally the absurd function can be used in an inline ternary etc. where alternatives like throw cannot.