Repository navigation
Never might be reasonably inferred to have properties also of type never. #23971
Description
Search Terms
type refinement properties, switch property, never properties
Suggestion
Right now never has no properties. This is somewhat at odds with mathematics where an empty set of elements is taken to have elements which trivially satisfy all predicates. There is probably a middle ground where there is a slightly richer type system for never, say, a never { type: never {}} type of values which should never happen which if they did happen would have a property named type. But I would find it easier if never was merely a sort of any which kept insidiously polluting its results with never: so a never function can take any sorts of arguments and returns a result also of type never , it is also an object whose properties have type never, and so forth.
Use Cases
So what's really at stake here is the fact that we know TypeScript code is type-insecure; we know this because it is ultimately at the end of the day JavaScript and that JavaScript is going to interact with other JavaScript which is type-insecure. I don't think it's too much of a stretch to say that manipulation of never values has only the one principal use-case: JavaScript gave us something which violated our TypeScript assumptions and we are trying to log it and clean up after it. (Maybe I'm missing something.)
Examples
Here's the example where I ran into this strange behavior; it is a switch that consumes a well-typed union:
type Report = {type: 'a', structure: StructureA} | {type: 'b', structure: StructureB}
switch (report.type) {
case 'a':
// do something with report.structure: StructureA
break;
case 'b':
// do something with report.structure: StructureB
break;
default:
assertImpossible(report.type);
}Here assertImpossible does more or less what you expect: it takes an argument of type never, casts it to a string, and throws a TypeError stating that the call-site cannot handle an argument with the given value. One expects the above to type-check as-is: "either report.type is 'type1' or it's 'type2' or JS gave us something which TS would have disallowed and we need to be cranky about this mismatch."
But this does not type-check as-is because TypeScript correctly infers report to have type never but then reports two errors on the reference to report.type: (a) TS cannot extract properties from an object of type never, (b) TS does not have a specification for the type of report.type so it infers the type any which is inconsistent with the caller, which expects an argument of type never. Instead one has to write assertImpossible(report) to satisfy TypeScript. Note that a type override in the above code would actually break the guarantee that assertImpossible is trying to provide.
Instead one needs to do some gymnastics to handle this case, function assertImpossible(value: never, hint?: string): never called as assertImpossible(report, 'type'), internally performing (report as any)[type] to get the actual value that failed, or perhaps just trying to JSON.stringify the report. The above code looks cleaner, though, and it just relies on the fact that the .type property is known to be there already -- if the report were null or so, then the error would have happened at switch (report.type) and/or at the call site assertImpossible(report.type) before the fetch of that property occurred.
Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript / JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. new expression-level syntax)
Activity
RyanCavanaugh commentedon May 8, 2018
Just to be clear - what would be an error in this world? It seems like 90% of the value of
neveris that you get an error when you try to use anevervalue, yet this proposal would seem to make it impossible for an error to appear. If you replaceassertImpossiblein your example withdeliverCustomerReportsit sure seems like the code as written has an error, but we can't tell which you meant.The proposed behavior is effectively the same as Flow's, which people seem to not really like:
facebook/flow#4134
facebook/flow#2723
facebook/flow#3062
facebook/flow#2774
ChrisDrostUSE commentedon May 8, 2018
Ryan Cavanaugh (@RyanCavanaugh) I am sorry but I think you may be confused about what I am proposing, or possibly about what
neveris.I think this because you said " It seems like 90% of the value of
neveris that you get an error when you try to use anevervalue," and that is just not true:It is therefore trivially true that under current TypeScript, yes, if you replace
assertImpossiblewithdeliverCustomReportsit type-checks, and I am not proposing to change this behavior, so it would still remain the same afterwards, yes.But if that's undesired behavior you should really raise that as a separate issue rather than commenting here about it? I think?
To be clear I am just saying that if
assertImpossibletakes a value of typeneverand I have a valuexof typenever, I should be able toassertImpossible(x.y).RyanCavanaugh commentedon May 8, 2018
Sorry, you're right that
neveris a valid input in that case.Going back to the original question - what is an observable difference between
neverandanyin your proposal?RyanCavanaugh commentedon May 8, 2018
Found the original - #13803
ChrisDrostUSE commentedon May 8, 2018
Are you asking on the value side or on the argument specification side?
On the value side, if you write
then an observable consequence is that you get a type error because
report.typehas typeanywhich is not assignable to an argument of typenever, whereas if you were to writereport.typethere would be no error.On the argument side, if you were to write,
this would only suppress one of the two given errors right now, and I think you are worried because I am proposing suppressing the other one, too, leaving no errors. But that there are no type errors when using
anyis not very surprising. Of course the above signature would violate the purpose of the above code usingassertImpossible, which is that if one later broadens the type totype Report = ... | {type: 'c', ...}then one gets a type error because the refined type ofreport.typeis now'c'which is not assignable to typenever.ChrisDrostUSE commentedon May 8, 2018
Ryan Cavanaugh (@RyanCavanaugh) reading the linked duplicate I almost wonder if the goal shouldn't be to make things work the way you thought they should -- that is, to disallow all manipulation of a
nevervalue. But I understand the reasoning given in that section -- that you want dead code to be discouraged but writable.I wonder whether you would accept a feature request for a different sort of block supporting this use-case, something like
One could then strengthen
neverso that it is not a subtype of any other type, and can only be manipulated in explicitimpossible {}blocks. It would then be released as part of TypeScript 3, say, being a breaking change to the language. (Or, possibly, to scope it to just one value,impossible (report) { ... }, where the semantics are maybe clearer: within the blockreportitself has typeany.)RyanCavanaugh commentedon May 8, 2018
Some sort of syntax to opt back in to a normal world makes sense. It looks like a special case of #10421 (block-level type assertions) since you'd presumably just "block-cast" the switch operand back to the unnarrowed type and then write your assertion/fall-back case
typescript-bot commentedon May 22, 2018
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.