Skip to content

Never might be reasonably inferred to have properties also of type never. #23971

Closed

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

  1. RyanCavanaugh commented on May 8, 2018

    @RyanCavanaugh
    Member

    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.

    Just to be clear - what would be an error in this world? It seems like 90% of the value of never is that you get an error when you try to use a never value, yet this proposal would seem to make it impossible for an error to appear. If you replace assertImpossible in your example with deliverCustomerReports it 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

  2. ChrisDrostUSE commented on May 8, 2018

    @ChrisDrostUSE
    Author

    Ryan Cavanaugh (@RyanCavanaugh) I am sorry but I think you may be confused about what I am proposing, or possibly about what never is.

    I think this because you said " It seems like 90% of the value of never is that you get an error when you try to use a never value," and that is just not true:

    let q: never;
    function deliverCustomerReports(report: string):string {
      return report + '!';
    }
    const value = deliverCustomerReports(q); // typechecks just fine, because `never` is a subtype of all types.

    It is therefore trivially true that under current TypeScript, yes, if you replace assertImpossible with deliverCustomReports it 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 assertImpossible takes a value of type never and I have a value x of type never, I should be able to assertImpossible(x.y).

  3. RyanCavanaugh commented on May 8, 2018

    @RyanCavanaugh
    Member

    Sorry, you're right that never is a valid input in that case.

    Going back to the original question - what is an observable difference between never and any in your proposal?

  4. RyanCavanaugh commented on May 8, 2018

    @RyanCavanaugh
    Member

    Found the original - #13803

  5. ChrisDrostUSE commented on May 8, 2018

    @ChrisDrostUSE
    Author

    Are you asking on the value side or on the argument specification side?

    On the value side, if you write

    default:
      const reportCopy: any = report;
      assertImpossible(reportCopy.type);

    then an observable consequence is that you get a type error because report.type has type any which is not assignable to an argument of type never, whereas if you were to write report.type there would be no error.

    On the argument side, if you were to write,

    function assertImpossible(value: any): never {
      throw new TypeError(`This switch statement does not know how to handle the value "${value}".`);
    }

    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 any is not very surprising. Of course the above signature would violate the purpose of the above code using assertImpossible, which is that if one later broadens the type to type Report = ... | {type: 'c', ...} then one gets a type error because the refined type of report.type is now 'c' which is not assignable to type never.

  6. ChrisDrostUSE commented on May 8, 2018

    @ChrisDrostUSE
    Author

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

    default: 
      impossible {
         // only typechecks when this code is unreachable, and within it all values of type `never` 
         // are treated as values of type `any`.
         throw switchFails(report.status)
      }
    

    One could then strengthen never so that it is not a subtype of any other type, and can only be manipulated in explicit impossible {} 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 block report itself has type any.)

  7. RyanCavanaugh commented on May 8, 2018

    @RyanCavanaugh
    Member

    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

  8. typescript-bot commented on May 22, 2018

    @typescript-bot
    Contributor

    Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

  9. locked and limited conversation to collaborators on Jul 31, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions