-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Treat expr satisfies never;
similarly to a call to a never-returning function in CFA
#61004
Comments
expr satisfies never;
similarly to a call to a never-returning functionexpr satisfies never;
similarly to a call to a never-returning function in CFA
This suggestion would break this common use case: type Value = "a" | "b"
function isValidValue(value: unknown): value is Value {
const asValue = value as Value;
switch (asValue) {
case "a":
case "b":
return true;
default:
asValue satisfies never;
return false;
}
} |
I'm personally OK with the breakage. In this case one would stop using |
Strong +1 to the objection in #61004 (comment)... A slight variant is a standard pattern for ensuring exhaustive switches in all cases (not only after a known-unsafe // maybe declared in another file, or even a 3rd party library.
type SomeUnionType = "a" | "b";
declare const x: SomeUnionType;
switch (x) {
case "a":
doOneThing();
break;
case "b":
doAnotherThing();
break;
default:
// ensure a TS error if another case is added to `SomeUnionType`, quite possibly with no diff to this file
x satisfies never;
// ensure a runtime error if the types are wrong at runtime (e.g. because package versions don't match, etc.)
throw new Error("Unexpected switch value: " + String(x));
} My mental model as a user is that Noting that the proposal also undermines itself: option satisfies never;
// β should have the same effect as:
// assertNever(option); yet, the example function noopMarkExhaustiveForTsCfa(x: never): never {
// why would you not throw, or at least log, here?
// non-exception behavior is Undefined Behavior in the program
}
switch (option) {
// ...
default:
noopMarkExhaustiveForTsCfa(option);
} |
I thought you were wise enough to understand I meant the effect to CFA. Otherwise how can I explicitly mention that That said, it's my bad to have used that implementation of function assertNever(value: never): never {
return value;
} I actually understand your objection. Especially when using TS to write a library, it may be reasonable to assume that types are not respected by users. So I view this as one of those applications VS libraries stuff. My opinition is rather biased to the application side. On this premise I still believe it's nice to let users believe types TS infers for you. TS already does type-based checks like flagging comparisions that always return true as error, and so on. I'd like to see |
hehe, I'll grant you that that was a bit of a cheap shot (intended to be a little tongue-in-cheek). I definitely respect that you have a thorough understanding of the topic!
The canonical counterexample though is on the application side. function handleDataFromServer(x: SomeEnumThatTheServerShouldBeSending) {
switch (x) {
// etc
default: {
x satisfies never;
console.error("unexpected case ", x);
}
}
} The point is that you might write an enum with known members that the server should be sending, but you still potentially need to handle not-yet-accounted-for new enum values since the server can add an enum value without it being a breaking change. |
Since TS doesn't have a distinction between exhaustive and non-exhaustive enums, it's important to be able to require a program to handle all known cases (via |
Ah, sorry for not understanding your point. I didn't think of that because I'd always validate value from potentially outdated/newer sources before it enters the type safe world. But I understand that's a too opinionated thought to support a suggestion like this. I'm still interested in what TS Team would say on this. |
Yeah this is definitely the right way to code π! But unfortunately it's common not to use enums this way in the wild. FWIW - my points on this issue are all specifically about the use of the Cheers! |
Nice idea! Random thought: declare value is never; π π€ Maybe |
π Search Terms
Control flow analysys, CFA, satisfies, never, return, termination, assert
β Viability Checklist
β Suggestion
In CFA,
expr satisfies never;
statements should be treated similarly to a call to a never-returning function, treating statements that follow as unreachable.I'm suggesting that TS enhance the
expr satisfies never;
pattern which is one of the ways to do the exhaustiveness check. It is a very popular technique used when dealing with discriminated union types. The ways often used are:Currently, 1 and 3 requires either throwing or returning to terminate the current (unreachable) execution branch. On the other hand, 2 automatically terminates it (assuming that the return type of
assertNever
isnever
).My suggestion is that 3 (
option satisfies never
) also work as a termination point of the current execution branch, so that the following would be possible:I think
expr satisfies never;
has several advantages compared to others:Also, notably,
expr satisfies never;
is explicit enough for the compiler to reason about as part of the CFA.In addition, I think this is also good from a theoretical perspective;
never
-returning functions are currently treated as a termination point of an execution branch because such functions can never return anything. Actually, this is not specific to functions. If you somehow have a value of typenever
, you have proven that this code isn't actually executed.expr satisfies never;
feels like a good, sensible way to declare that you have a proof.π Motivating Example
Playground
π» Use Cases
1. What do you want to use this for?
As shown above, for exhaustiveness checks.
2. What shortcomings exist with current approaches?
Live with less optimal (from different aspects) ways.
3. What workarounds are you using in the meantime?
The text was updated successfully, but these errors were encountered: