5 ms·
99% of my use of `satisfies` is to type-check exhaustivity in `switch` statements: type Foo = 'foo' | 'bar'; const myFoo: Foo = 'foo'; switch
by amake 11mo 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
}
- inlined 11mo agoNice. 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 11mo agoI 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 11mo agoI still keep my assertNever function because it will handle non-exhaustiveness at runtime.
- ervine 11mo agohttps://typescript-eslint.io/rules/switch-exhaustiveness-check/ https://typescript-eslint.io/rules/switch-exhaustiveness-che... if that is something you're not aware of!
- mckirk 11mo agoI 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.
- mquander 11mo agoThat's great, I'm going to use that one in the future.
- rezonant 11mo agoThat's very clever!
- mrlowlevel 11mo agoWe 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}`) } ```
- jstanley 11mo agoThe fact that there can be runtime type errors that were proven impossible at compile time is why I will never enjoy TypeScript.
- Defletter 11mo agoIsn'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 https://openjdk.org/jeps/361
- 11mo ago
- your_fin 11mo agoI 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 https://github.com/gvergnaud/ts-pattern
- klinch 11mo agoTIL.
- Normal_gaussian 11mo agoThis 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.