Skip to content

A variable of type never should allow anything to be done with it #13803

Closed

Description

@pelotom

My understanding of never is that it's the bottom type, and therefore a subtype/intersection of all other types. As such, shouldn't the following type check?

function f(foo: never) {
  return foo.bar // Type error: Property 'bar' does not exist on type 'never'.
}

Since we can never actually obtain a value of type never as long as we haven't subverted the type system somewhere, there's nothing wrong in accessing arbitrary properties on it. From absurdity, anything follows, right?

One can already write this function, which type checks:

function coerceNever(foo: never): { bar: string } {
  return foo
}

and then this works:

function f(foo: never) {
  return coerceNever(foo).bar
}

Activity

  1. changed the title [-]A variable of type never should allow anything[/-] [+]A variable of type never should allow anything to be done with it[/+] on Feb 1, 2017
  2. RyanCavanaugh commented on Feb 1, 2017

    @RyanCavanaugh
    Member

    there's nothing wrong in accessing arbitrary properties on it

    Conversely, accessing properties on a value that will never be seen is a big red flag. What is gained from this change?

    never more often shows up when people "accidently" exhaust a union type, rather than the very rare case of declaring something of type never. How would we report errors in that situation if never were effectively any ?

  3. pelotom commented on Feb 1, 2017

    @pelotom
    Author

    never more often shows up when people "accidently" exhaust a union type, rather than the very rare case of declaring something of type never. How would we report errors in that situation if never were effectively any ?

    Shouldn't the situation in which a union type has been "accidentally exhausted" result in unreachable code errors, regardless of whether one tries to access properties of a variable whose type has been narrowed to never? For example:

    if (false)
      console.log('hello')

    Here the console.log is rightly flagged as Unreachable code detected. However in this situation:

    function f(x: 'a'|'b'|'c') {
      if (x === 'a') {
        //...
      } else if (x === 'b') {
        //...
      } else if (x === 'c') {
        //...
      } else {
        console.log('hello')
      }
    }

    even though the console.log is unreachable, it's not currently flagged as such. Only if one tries to make use of x.length or somesuch in the final branch will it be flagged as an error currently. So making property accesses on never a type error just for this case seems like a hack which doesn't fully solve the problem it's supposed to.

  4. RyanCavanaugh commented on Feb 1, 2017

    @RyanCavanaugh
    Member

    No one would want the exhaustion of a union to result in an error to result in an unreachable code warning, because unreachable code warnings have no way to be silenced, but it's still very common to write Debug.fail or throw or other sorts of code in those positions. I mean, what you're basically asking for there is for the declaration of f(x: never) to be itself an error, since by your own argument that function can't be called.

    The difference between our syntactic unreachability analysis and the never type is that we are 100% confident in the unreachability of if (false) console.log('hello');. We can't be 100% confident about f(x: 'a' | 'b' | 'c') because non-TS code exists, any exists, type assertions exist, etc.

  5. vkurchatkin commented on Feb 1, 2017

    @vkurchatkin
    Contributor

    In Flow bottom works as upper bound of any, i.e. you can do anything with it, but can't directly assign anything to it.

    Here is a practical example why it's useful:

    type Union =
        | { t: 'Foo' }
        | { t: 'Bar' }
    
    declare function test(): Union;
    
    const x = test();
    switch (x.t) {
        case 'Foo':
            console.log('This is Foo');
            break;
        case 'Bar':
            console.log('This is Bar');
            break;
        default:
            console.log(`Unknown type: ${x.t}`);
    }

    While default case is unreachable from type checker's point of view, in real life it still might happen because of incorrect typings or something like that

  6. pelotom commented on Feb 1, 2017

    @pelotom
    Author

    Ryan Cavanaugh (@RyanCavanaugh) fair point. I still think disallowing property accesses on never is a hack which hurts as much as it helps, as Vladimir Kurchatkin (@vkurchatkin)'s example shows.

  7. RyanCavanaugh commented on Feb 1, 2017

    @RyanCavanaugh
    Member

    Anecdotally, when we added the never type, it found about a dozen bugs in our partner codebases where people did access members of things of type never not realizing the code was unreachable (i.e. there was a mistake in the logic of the function). It'd be a shame to have missed those bugs for the sake of removing a type assertion in expected-unreachable code.

  8. RyanCavanaugh commented on Feb 1, 2017

    @RyanCavanaugh
    Member

    The Flow example is extremely surprising to me.

    type Union =
        | { t: 'Foo' }
        | { t: 'Bar' }
    
    declare function test(): Union;
    
    function fn(x: number) { }
    const x = test();
    switch (x.t) {
        case 'Foo':
            console.log('This is Foo');
            break;
        case 'Bar':
            console.log('This is Bar');
            break;
        default:
            // No error?
            const wat = x.flarp.substring(3);
            // No error?
            fn(wat);
    }

    I mean, the big selling point is supposed to be a sound type system, but you write code that should be unreachable (oops?) and then access an undeclared property (oops again?) and pass that to an explicitly-typed function (oops?) with no warnings? Really? Why shouldn't I have to acknowledge in some form that I'm doing stuff with a value that I know is outside of its declared domain? Instead it's effectively any and the typechecker is silently letting me do whatever I want. That seems harmful, not helpful.

  9. pelotom commented on Feb 1, 2017

    @pelotom
    Author

    Notice that 2 out of your 3 oopses are not caught by current TypeScript. This is why I think an unreachable code warning would be ideal, if you could disable it with a pragma to say "I acknowledge that I'm in untyped land here, just covering my bases".

  10. vkurchatkin commented on Feb 1, 2017

    @vkurchatkin
    Contributor

    I mean, the big selling point is supposed to be a sound type system,

    I'd argue that this behaviour is both sound and consistent from type system's point of view. If code is considered unreachable then its content is irrelevant

    Formally ∀X: ⊥ <: X, hence Γ ⊢ e : ⊥ -> Γ ⊢ e : X. This means that every operation on some type T must be allowed on bottom type

  11. Yogu commented on Jun 20, 2017

    @Yogu
    Contributor

    I often write a switch statement that has a case for each option of a union. In the default branch, I prefer to throw an execption that has the swiched value in its message, so that debugging is easier. This is the same as Vladimir Kurchatkin (@vkurchatkin)'s flow example. Currently, this pattern requires a type assertion.

    switch (node.kind) {
        case 'Field':
            //...
        case 'FragmentSpread':
        case 'InlineSpread':
            //...
        default:
            throw new Error(`Unexpected node kind: ${(<any>node).kind}`);
    }
    

    I don't think reverting to something like any for node would be good, because then, the code completion and name checkinf for kind would be gone. I would prefer to revert any narrowing done by the switch statement and end up with the original type at the beginning - then I could access kind normally.

  12. added
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    and removed
    Needs InvestigationThis issue needs a team member to investigate its status.
    on Jun 20, 2017
  13. RyanCavanaugh commented on Jun 20, 2017

    @RyanCavanaugh
    Member

    So we get bugs like #15079 all the time (that one doesn't produce a never, but many do). "I don't have to write a type assertion in unreachable code" is not obviously a feature because people often write code that they don't realize is unreachable.

    I'd recommend the pattern outlined here https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript if you want type-assertion-free code.

  14. locked and limited conversation to collaborators on Jun 19, 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

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions