-
Notifications
You must be signed in to change notification settings - Fork 12.8k
“A type predicate's type must be assignable to its parameter's type” does not always kick in #32541
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
Comments
I'm afraid I don't understand what the problem is. You can take your "good" example and do if (isDog(cat)) {
// if you're in here, cat is Cat & Dog
cat.bark();
cat.meow();
} which behaves exactly like the Also, nothing stops const catdog: Cat & Dog = {
species: "catdog",
bark() {
console.log("BARK!");
},
meow() {
console.log("MEOW!");
}
};
const cat: Cat = catdog; so it's not even that crazy to be testing I'm not sure what issue you're trying to report. Maybe I'm misunderstanding or something? |
Hey @jcalz, thanks for your answer. Let me put it this way: If that raises a compile-time error function INCORRECT_isDog<T extends Animal>(candidate: T): candidate is Dog { then why this doesn't? function UNSAFE_isDog<T extends Animal>(candidate: T): candidate is T & Dog { This modification shouldn't change anything. It takes what we implicitly know to be true — the fact
You can't, because If you're worried about overlaps and things like interface Dog extends Animal {
bark(): void;
meow: never;
}
interface Cat extends Animal {
meow(): void;
bark: never;
} |
If you look at the type lattice you have something like this: Your first type guard is effectively taking the red line, which is bad because it doesn't respect the lattice structure. You shouldn't be able to convert a Your second type guard respects the lattice structure by taking the meet (intersection) of Using the second type guard forces the user to write a more precise type and therefore manifest any nonsensical type guards that produce function silly<T extends number>(candidate: T): candidate is T & boolean {
…
} It's effectively the same reasoning for type casts: |
@jack-williams Thank you for your explanation. To sum up: which one — |
If there is a meaningful Let's use your latter mutually-inconsistent definitions of if (isDog(cat)) {
cat.bark(); // error, is never now
cat.meow(); // error, is never now
cat.species.charAt(0); // okay, still string
} So Actually I'd say you want If you make sure that If you really want to prevent people from passing in known function strictIsDog<T extends Dog extends T ? unknown : never>( // like <T super Dog>
candidate: Dog | T // if Dog extends T then Dog | T is T
): candidate is Dog { // compiler recognizes that Dog | T can narrow to T
return "bark" in candidate;
}
if (strictIsDog(animal)) {} // okay
if (strictIsDog(dog)) {} // okay
if (strictIsDog(mixed)) {} // okay
if (strictIsDog(cat)) {} // error!
// ~~~ <-- Cat is not assignable to Dog Maybe that will behave reasonably for you? Not sure. Good luck! |
@jack-williams @jcalz thank you again for taking the time to explain how the compiler thinks in such an elaborate way. This is some blog post material! I think I get why adding a seemingly neutral I wrote down how I understand it using layman's terms for anyone finding this issue in the future. /**
* Error: "A type predicate's type must be assignable to its parameter's type".
*
* (Right-hand side must be a subtype of the left-hand side.)
*/
declare function isDate(candidate: Date): candidate is string; /**
* Now it's correct within the laws of the type system, but makes zero practical sense,
* because there exists no runtime representation of the type `Date & string`.
*
* The type system doesn't care whether a type can be represented in runtime though.
*
* Adding the seemingly neutral `candidate is typeof candidate` bit makes TypeScript
* happy because all it cares about is for the right-hand side to be a subtype of the left-hand side.
* In this case, `Date & string` is more specific than (is a subtype of) `Date`.
*
* As a design decision, TypeScript does not collapse that type to `never` (although it could).
*/
declare function isDate(candidate: Date): candidate is typeof candidate & string; If there is no bug, and there are no plans to reduce empty cases to Thanks for the help! |
The |
What I don't understand is why you need to make it explicit? Given:
Without resorting to |
|
TypeScript Version: 3.6.0-dev.20190723
Search Terms: type guard, subtype, instantiated with a different subtype, identity
Code
Expected behavior:
UNSAFE_isDog
would fail to compile just likeINCORRECT_isDog
. AssertingT
to be of typeT
in the return type should not trick the compiler into thinking everything is fine.Actual behavior:
UNSAFE_isDog
compiles fine, which leads to runtime exceptions.Playground Link: Click
Related issues: #24935, #30240
You can see the full use case below 👇.
Use case
Let's take a hierarchy like this:
The good
If we were to check whether a piece of data is a
Dog
, we could write a type guard.Its signature could look like this:
Our
isDog
would probably check for the existence of thespecies
property to make sure it'sdefined and of type
string
. So far so good.The bad
Another way this can be done is by mixing runtime validation with compile-time validation. For
example, if we already know if something is an
Animal
, then it might be tempting to do less workby checking only for the properties specific to
Dog
s.This would require the argument to be an
Animal
.This won't work! We get a compile-time error.
The error message makes perfect sense, because without the error we would be allowed to do:
The ugly
However, if we tweak the signature, the error message goes away.
This fails in runtime.
Asserting the argument to be itself made the type checker shut up. It didn't make the code any safer though. Is that intended?
I'm aware that
UNSAFE_isDog
looks horrible and feels like an abuse of the type systembut I'm having trouble convincing my team that
UNSAFE_isDog
is not the best way to go.The text was updated successfully, but these errors were encountered: