Skip to content

Type narrowing generics/unknown doesn't seem to work with typeof and null checkΒ #43997

Description

@43081j

Bug Report

πŸ”Ž Search Terms

typeof object, null checks, narrowing object, narrow generics

πŸ•— Version & Regression Information

All

  • This is the behavior in every version I tried

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

function func<T>(obj: T): boolean {
  if (obj && (typeof obj === 'object') && obj.hasOwnProperty('xyz')) {
    return true;
  }
  return false;
}

declare const testVariable: unknown;

if (testVariable !== null && (typeof testVariable === 'object') && testVariable.hasOwnProperty('xyz')) {
}

if ((typeof testVariable === 'object') && testVariable !== null && testVariable.hasOwnProperty('xyz')) {
}

πŸ™ Actual behavior

The generic function doesn't infer that obj must be an object of some sort.

The first if doesn't infer that testVariable can't be null.

πŸ™‚ Expected behavior

The generic function should infer that T is an object because we did a null check and checked typeof.

Both ifs should infer that testVariable can't be null.

Notes

Is this just me missing some 'known behaviour' typescript has? the two conditions seem to depend on ordering, maybe thats just a known thing i was unaware of (i.e. by design)?

Activity

  1. MartinJohns commented on May 7, 2021

    @MartinJohns
    Contributor

    Duplicate of #28131.

  2. 43081j commented on May 7, 2021

    @43081j
    Author

    The second part of this is. There are two issues i would say, the generic function can be re-ordered to have the same order and will still error.

    e.g.

    function func<T>(obj: T): boolean {
      // Errors because `hasOwnProperty` doesn't exist on T, doesn't know it is an object
      if ((typeof obj === 'object') && obj !== null && obj.hasOwnProperty('xyz')) {
        return true;
      }
      return false;
    }
  3. MartinJohns commented on May 7, 2021

    @MartinJohns
    Contributor
    T extends Object
    

    Otherwise you can pass in this object: { hasOwnProperty: () => "example" }.

  4. 43081j commented on May 7, 2021

    @43081j
    Author

    T doesn't extend object...

    function func<T>(obj: T): boolean {
      if ((typeof obj === 'object') && obj !== null && obj.hasOwnProperty('xyz')) {
        return true;
      }
      return false;
    }
    
    func('foo'); // false
    func(123); // false
    func({xyz: 123}); // true
    // etc etc
  5. MartinJohns commented on May 7, 2021

    @MartinJohns
    Contributor

    See my update. If you don't make T extends Object you can pass in an object like { hasOwnProperty: () => "example" }.

    And 'foo' and 123 both extend Object.

    function func<T extends Object>(_val: T) {}
    
    // Works fine
    func('example')
    func(123)
    func(true)
    
  6. 43081j commented on May 7, 2021

    @43081j
    Author

    out of interest, why isn't that behaviour the same without generics?

    unknown could also be { hasOwnProperty: () => "example" } could it not?

    const foo: unknown = {hasOwnProperty: () => "blah"};
    
    if (typeof foo === 'object' && foo !== null && foo.hasOwnProperty('blah')) {
      // WORKS
    }

    so why is it any different just because we have a T rather than unknown? which is just as lacking in type

    i suppose because T means the compiler could have inferred the type from higher up the chain and would know hasOwnProperty is something different here, whereas with unknown it really has no clue so is a bit dumber about it. so only because the compiler knows more in the case of generics it disallows this

  7. MartinJohns commented on May 7, 2021

    @MartinJohns
    Contributor

    I don't know about the specifics, you'd need to wait for the TypeScript team or someone else more knowledgable.

    But your guess sounds like a good one to me. It's similar to this example:

    function func<T extends Object>(value: T) {
      if (isTest(value)) { value; }
    }
    
    type Test = { test: string }
    function isTest(val: unknown): val is Test { return true }
    

    value is not narrowed to Test, it's narrowed to T & Test. The generic type argument is kept.

  8. RyanCavanaugh commented on May 11, 2021

    @RyanCavanaugh
    Member

    There are some weird corner cases here I don't recall that lead to this. It's certainly not desirable.

  9. locked as resolved and limited conversation to collaborators on Oct 21, 2025
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

    BugA bug in TypeScriptFix AvailableA PR has been opened for this issue

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions