Skip to content

Base class is narrowed to never after User-defined Type Guard on subclass #57193

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

Closed
gluxon opened this issue Jan 26, 2024 · 3 comments
Closed
Labels
Question An issue which isn't directly actionable in code

Comments

@gluxon
Copy link
Contributor

gluxon commented Jan 26, 2024

πŸ”Ž Search Terms

  • user defined type guard
  • narrowing
  • subclass

πŸ•— Version & Regression Information

  • This reproduces on v5.3.3.
  • This is the behavior in every version I tried, going back to v3.3.3333.

⏯ Playground Link

https://www.typescriptlang.org/play?ssl=6&ssc=1&pln=7&pc=1#code/MYGwhgzhAECCB2BLAtmE0DeAoa0AOArgEYiLDSoDWApgHID2iE1AFAJSY67QBO1ALgR7xoAIniNmorgF8scrKEgwAIvQDm0agA9+1eABMYCFGk65CJMtAj8w-a0xZgkqEAC44rtG08vT6EzQaprY3LwCQiL+btCI8LYuwNT0AGZeAbLyWFipBPDADvQierbO3h4Zbr42-DzxoVyI6SwhAHROMT4cYdx8gsLQXSBtVHSSrGxZuP1RQxWjYDQMTJNcAPTr4bgAegD82bn5hYjF0KX8AEzlAZ4m1Z629fCNuM3QN7HxiQUp6SE9LgzSKDYaLZYTdjTCIDaILMYrZhQ3CbbbQfbZIA

πŸ’» Code

class Animal {
  public makeNoise() {
    return "noise"
  }
}

class Dog extends Animal {
  public static is(animal: Animal): animal is Dog {
    return animal instanceof Animal
  }
}

function test(animal: Animal): string {
  if (Dog.is(animal)) {
    return animal.makeNoise()
  }
  return animal.makeNoise()
  //      ^?
}

function test2(animal: Animal): string {
  if (animal instanceof Dog) {
    return animal.makeNoise()
  }
  return animal.makeNoise()
  //      ^?
}

Edit: Switched animal is Animal to animal is Dog. Caught by @Andarist #57193 (comment).

πŸ™ Actual behavior

Under the test() function, the animal object is narrowed to never.

πŸ™‚ Expected behavior

The animal object is not narrowed kept as Animal.

Additional information about the issue

This seems to be different when using a user defined type guard vs an inline instanceof check which surprised me.

@Andarist
Copy link
Contributor

Your Animal and Dog classes are structurally identical so TS sees them as compatible. On top of that, your static Dog.is tries to check if animal is Animal but you probably have meant animal is Dog.

After "correcting" both you get the expected results: TS playground . You have just observed a normal subtype elimination process - in your version you have "removed" Animal from Animal and thus you have been left out with never in the else branch

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Jan 26, 2024
@gluxon
Copy link
Contributor Author

gluxon commented Jan 26, 2024

On top of that, your static Dog.is tries to check if animal is Animal but you probably have meant animal is Dog.

Thanks for catching that. I did intend to write animal is Dog here. Going to edit the problem description for that fix.

Your Animal and Dog classes are structurally identical so TS sees them as compatible. After "correcting" both you get the expected results: TS playground .

I also noticed that adding a new private field to Dog changes the narrowing behavior. I was debating whether to commit that workaround to an internal codebase.

You have just observed a normal subtype elimination process - in your version you have "removed" Animal from Animal and thus you have been left out with never in the else branch

I think the surprising bit is that this happens when removing Dog from Animal as well. I'm realizing from your description that UDFs are structural checks, and there's a separate "this-based type guard that would be better for classes.

In any case, the behavior seems intended. Thank you for the help!

@gluxon gluxon closed this as completed Jan 26, 2024
@fatcerberus
Copy link

All type checks involving objects are structural, either directly or indirectly. Even the ones that look nominal, like classes with private members (nominality is emulated by having the source location of the private field be treated as part of the "structure", which can have some surprising effects, e.g. #56146 and especially #55235).

Pervasive structural typing is why you get behavior like described here:
https://github.com/microsoft/TypeScript/wiki/FAQ#why-do-these-empty-classes-behave-strangely

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

4 participants