Skip to content

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

Merged
merged 7 commits into from
Mar 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20147,6 +20147,12 @@ namespace ts {
if (right.kind === SyntaxKind.TypeOfExpression && isStringLiteralLike(left)) {
return narrowTypeByTypeof(type, <TypeOfExpression>right, operator, left, assumeTrue);
}
if (isConstructorAccessExpression(left)) {
return narrowTypeByConstructor(type, left, operator, right, assumeTrue);
}
if (isConstructorAccessExpression(right)) {
return narrowTypeByConstructor(type, right, operator, left, assumeTrue);
}
if (isMatchingReference(reference, left)) {
return narrowTypeByEquality(type, operator, right, assumeTrue);
}
Expand Down Expand Up @@ -20432,6 +20438,59 @@ namespace ts {
return getTypeWithFacts(mapType(type, narrowTypeForTypeofSwitch(impliedType)), switchFacts);
}

function narrowTypeByConstructor(type: Type, constructorAccessExpr: AccessExpression, operator: SyntaxKind, identifier: Expression, assumeTrue: boolean): Type {
// Do not narrow when checking inequality.
if (assumeTrue ? (operator !== SyntaxKind.EqualsEqualsToken && operator !== SyntaxKind.EqualsEqualsEqualsToken) : (operator !== SyntaxKind.ExclamationEqualsToken && operator !== SyntaxKind.ExclamationEqualsEqualsToken)) {
return type;
}

// In the case of `x.y`, a `x.constructor === T` type guard resets the narrowed type of `y` to its declared type.
if (!isMatchingReference(reference, constructorAccessExpr.expression)) {
return declaredType;
}

// Get the type of the constructor identifier expression, if it is not a function then do not narrow.
const identifierType = getTypeOfExpression(identifier);
if (!isFunctionType(identifierType) && !isConstructorType(identifierType)) {
return type;
}

// Get the prototype property of the type identifier so we can find out its type.
const prototypeProperty = getPropertyOfType(identifierType, "prototype" as __String);
if (!prototypeProperty) {
return type;
}

// Get the type of the prototype, if it is undefined, or the global `Object` or `Function` types then do not narrow.
const prototypeType = getTypeOfSymbol(prototypeProperty);
const candidate = !isTypeAny(prototypeType) ? prototypeType : undefined;
if (!candidate || candidate === globalObjectType || candidate === globalFunctionType) {
return type;
}

// If the type that is being narrowed is `any` then just return the `candidate` type since every type is a subtype of `any`.
if (isTypeAny(type)) {
return candidate;
}

// Filter out types that are not considered to be "constructed by" the `candidate` type.
return filterType(type, t => isConstructedBy(t, candidate));

function isConstructedBy(source: Type, target: Type) {
// If either the source or target type are a class type then we need to check that they are the same exact type.
// This is because you may have a class `A` that defines some set of properties, and another class `B`
// that defines the same set of properties as class `A`, in that case they are structurally the same
// type, but when you do something like `instanceOfA.constructor === B` it will return false.
if (source.flags & TypeFlags.Object && getObjectFlags(source) & ObjectFlags.Class ||
target.flags & TypeFlags.Object && getObjectFlags(target) & ObjectFlags.Class) {
return source.symbol === target.symbol;
}

// For all other types just check that the `source` type is a subtype of the `target` type.
return isTypeSubtypeOf(source, target);
}
}

function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
const left = getReferenceCandidate(expr.left);
if (!isMatchingReference(reference, left)) {
Expand Down
7 changes: 7 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4426,6 +4426,13 @@ namespace ts {
return isPropertyAccessExpression(node) && isEntityNameExpression(node.expression);
}

export function isConstructorAccessExpression(expr: Expression): expr is AccessExpression {
return (
isPropertyAccessExpression(expr) && idText(expr.name) === "constructor" ||
isElementAccessExpression(expr) && isStringLiteralLike(expr.argumentExpression) && expr.argumentExpression.text === "constructor"
);
}

export function tryGetPropertyAccessOrIdentifierToString(expr: Expression): string | undefined {
if (isPropertyAccessExpression(expr)) {
const baseStr = tryGetPropertyAccessOrIdentifierToString(expr.expression);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(66,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
Property 'property1' does not exist on type 'number'.
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(73,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
Property 'property1' does not exist on type 'number'.
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(80,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
Property 'property1' does not exist on type 'number'.
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(87,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
Property 'property1' does not exist on type 'number'.
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(94,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
Property 'property1' does not exist on type 'number'.
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(101,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
Property 'property1' does not exist on type 'number'.
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(108,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
Property 'property1' does not exist on type 'number'.
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(115,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
Property 'property1' does not exist on type 'number'.


==== tests/cases/compiler/typeGuardConstructorClassAndNumber.ts (8 errors) ====
// Typical case
class C1 {
property1: string;
}

let var1: C1 | number;
if (var1.constructor == C1) {
var1; // C1
var1.property1; // string
}
else {
var1; // number | C1
}
if (var1["constructor"] == C1) {
var1; // C1
var1.property1; // string
}
else {
var1; // number | C1
}
if (var1.constructor === C1) {
var1; // C1
var1.property1; // string
}
else {
var1; // number | C1
}
if (var1["constructor"] === C1) {
var1; // C1
var1.property1; // string
}
else {
var1; // number | C1
}
if (C1 == var1.constructor) {
var1; // C1
var1.property1; // string
}
else {
var1; // number | C1
}
if (C1 == var1["constructor"]) {
var1; // C1
var1.property1; // string
}
else {
var1; // number | C1
}
if (C1 === var1.constructor) {
var1; // C1
var1.property1; // string
}
else {
var1; // number | C1
}
if (C1 === var1["constructor"]) {
var1; // C1
var1.property1; // string
}
else {
var1; // number | C1
}

if (var1.constructor != C1) {
var1; // C1 | number
var1.property1; // error
~~~~~~~~~
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
!!! error TS2339: Property 'property1' does not exist on type 'number'.
}
else {
var1; // C1
}
if (var1["constructor"] != C1) {
var1; // C1 | number
var1.property1; // error
~~~~~~~~~
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
!!! error TS2339: Property 'property1' does not exist on type 'number'.
}
else {
var1; // C1
}
if (var1.constructor !== C1) {
var1; // C1 | number
var1.property1; // error
~~~~~~~~~
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
!!! error TS2339: Property 'property1' does not exist on type 'number'.
}
else {
var1; // C1
}
if (var1["constructor"] !== C1) {
var1; // C1 | number
var1.property1; // error
~~~~~~~~~
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
!!! error TS2339: Property 'property1' does not exist on type 'number'.
}
else {
var1; // C1
}
if (C1 != var1.constructor) {
var1; // C1 | number
var1.property1; // error
~~~~~~~~~
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
!!! error TS2339: Property 'property1' does not exist on type 'number'.
}
else {
var1; // C1
}
if (C1 != var1["constructor"]) {
var1; // C1 | number
var1.property1; // error
~~~~~~~~~
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
!!! error TS2339: Property 'property1' does not exist on type 'number'.
}
else {
var1; // C1
}
if (C1 !== var1.constructor) {
var1; // C1 | number
var1.property1; // error
~~~~~~~~~
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
!!! error TS2339: Property 'property1' does not exist on type 'number'.
}
else {
var1; // C1
}
if (C1 !== var1["constructor"]) {
var1; // C1 | number
var1.property1; // error
~~~~~~~~~
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
!!! error TS2339: Property 'property1' does not exist on type 'number'.
}
else {
var1; // C1
}

Loading