Skip to content
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

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

Closed
43081j opened this issue May 7, 2021 · 8 comments Β· Fixed by #49119
Closed

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

43081j opened this issue May 7, 2021 · 8 comments Β· Fixed by #49119
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Milestone

Comments

@43081j
Copy link

43081j commented May 7, 2021

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)?

@MartinJohns
Copy link
Contributor

Duplicate of #28131.

@43081j
Copy link
Author

43081j commented May 7, 2021

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;
}

@MartinJohns
Copy link
Contributor

MartinJohns commented May 7, 2021

T extends Object

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

@43081j
Copy link
Author

43081j commented May 7, 2021

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

@MartinJohns
Copy link
Contributor

MartinJohns commented May 7, 2021

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)

@43081j
Copy link
Author

43081j commented May 7, 2021

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

@MartinJohns
Copy link
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.

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label May 11, 2021
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone May 11, 2021
@RyanCavanaugh
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants