-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Implement constructor type guard #32774
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
Implement constructor type guard #32774
Conversation
@weswigham this LGTM - please merge if you agree |
Addressed @weswigham's comments. |
Looks like some tests are failing - you may need to run |
Ah ok, I forgot to rebase onto master. Looks like some symbol positions changed. |
- Do not limit constructor expression to only identifiers - Fix `assumeTrue` and operator no-narrow check - Use better way to check that identifier type is a function - Loosen restriction on what expr is left of ".constructor" - Update typeGuardConstructorClassAndNumber test to include else cases
So, showerthought here, but.... What do we do when const x = { constructor: String };
if (x.constructor === String) {
x.trim(); // runtime error
} Or any other instance where the constructor property isn't inherited from |
@weswigham, just tried it out with my local build and it looks like that specific case won't be an issue. `x` will have a type of `never` inside the if so you will get a compile time error. This is because we aren't comparing against the type of `x`'s constructor, but rather against the type of `x`, and the prototype type of the identifier right of the triple-equals directly.
I haven't thought of a case yet where this will break but I'll definitely keep thinking about it.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RyanCavanaugh you can give this another look when you're ready, but as far as the code goes, I think this is pretty good.
I came here to request this feature, as I'm doing |
Same here, this is really an awesome feature and glad the author and the Typescript team going to support it 🍰 |
Bump! |
I'd like to suggest adding an alternative Why / where am I coming from: I worked once in a weird environment (I think an nw.js app) where my element objects were defined in a different context than globally available constructors, so stuff like actualInputElement instanceof HTMLInputElement would always return Is it too unsound to even be considered? :) Because it's working perfectly on html elements and I don't have to be afraid of weird environments with colliding contexts. |
I had a similar situation inside an HTMLObjectElement. I think |
Fixes #23274
tl;dr: Implements constructor check type guards in the form of
x.constructor === T
.This PR adds a new constructor equality check type guard. It specifically works on a set of binary expressions in the following syntactic forms:
x.constructor === T
x.constructor == T
x["constructor"] === T
x["constructor"] == T
Narrowing is done via the added
narrowTypeByConstructor
function. This type guard only operates on==
and===
so if it finds the expression is using a not-equals operator then it returns the type without any narrowing done.After that it uses
isMatchingReference
to presumably check that we are still referring to thex
part of thex.constructor === T
expression. I'm not certain on how this works exactly, but the use case is to ensure onlyx
has the narrowed type in the if body as opposed tox.y
having the narrowed type.Once that check is complete it moves on to getting the type referred to by
T
. This type eventually turns into ourcandidate
type if it is not a direct reference to theglobalObjectType
orglobalFunctionType
. This does introduce a shortcoming of the narrowing as it prevents narrowing using aT
ofObject
orFunction
.If the type that is being narrowed is
any
, then it can assume thecandidate
type is a subtype ofany
and so we just narrow directly tocandidate
. I think this is a safe assumption but maybe not? Also there is no need to check forunknown
as you would receive an error attempting to accessx.constructor
ifx
's type isunknown
.Finally it returns the result of the
filterType
function which uses a functionisConstructedBy
to do the filtering. If none of the types match we end up withnever
as the flow type.isConstructedBy
This function checks if either the
source
ortarget
type are Object types with theClass
ObjectFlag
. If this is the case then we return if the symbols ofsource
andtarget
type are===
. This is needed because you may have two classes with the same structure and TypeScript will see them as the same type. There was no way I could find that would check this. I think this also introduces the side effect of having pseudo nominal typing in the language.For all other cases it just checks that the
source
type is a subtype oftarget
type.Caveats and questions
T
ofObject
orFunction
.source
andtarget
types a good way to check that they are referring to the exact same class?