Skip to content

Commit ae3d28b

Browse files
Implement constructor type guard (#32774)
* Implement constructor type guard * Fix code review issues for constructor type guard. - 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 * Fix grammar & spacing in `narrowTypeByConstructor` * fix bad merge * switch (back?) to crlf * update baselines Co-authored-by: Nathan Shively-Sanders <[email protected]>
1 parent b78ef30 commit ae3d28b

24 files changed

+2487
-0
lines changed

src/compiler/checker.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20147,6 +20147,12 @@ namespace ts {
2014720147
if (right.kind === SyntaxKind.TypeOfExpression && isStringLiteralLike(left)) {
2014820148
return narrowTypeByTypeof(type, <TypeOfExpression>right, operator, left, assumeTrue);
2014920149
}
20150+
if (isConstructorAccessExpression(left)) {
20151+
return narrowTypeByConstructor(type, left, operator, right, assumeTrue);
20152+
}
20153+
if (isConstructorAccessExpression(right)) {
20154+
return narrowTypeByConstructor(type, right, operator, left, assumeTrue);
20155+
}
2015020156
if (isMatchingReference(reference, left)) {
2015120157
return narrowTypeByEquality(type, operator, right, assumeTrue);
2015220158
}
@@ -20432,6 +20438,59 @@ namespace ts {
2043220438
return getTypeWithFacts(mapType(type, narrowTypeForTypeofSwitch(impliedType)), switchFacts);
2043320439
}
2043420440

20441+
function narrowTypeByConstructor(type: Type, constructorAccessExpr: AccessExpression, operator: SyntaxKind, identifier: Expression, assumeTrue: boolean): Type {
20442+
// Do not narrow when checking inequality.
20443+
if (assumeTrue ? (operator !== SyntaxKind.EqualsEqualsToken && operator !== SyntaxKind.EqualsEqualsEqualsToken) : (operator !== SyntaxKind.ExclamationEqualsToken && operator !== SyntaxKind.ExclamationEqualsEqualsToken)) {
20444+
return type;
20445+
}
20446+
20447+
// In the case of `x.y`, a `x.constructor === T` type guard resets the narrowed type of `y` to its declared type.
20448+
if (!isMatchingReference(reference, constructorAccessExpr.expression)) {
20449+
return declaredType;
20450+
}
20451+
20452+
// Get the type of the constructor identifier expression, if it is not a function then do not narrow.
20453+
const identifierType = getTypeOfExpression(identifier);
20454+
if (!isFunctionType(identifierType) && !isConstructorType(identifierType)) {
20455+
return type;
20456+
}
20457+
20458+
// Get the prototype property of the type identifier so we can find out its type.
20459+
const prototypeProperty = getPropertyOfType(identifierType, "prototype" as __String);
20460+
if (!prototypeProperty) {
20461+
return type;
20462+
}
20463+
20464+
// Get the type of the prototype, if it is undefined, or the global `Object` or `Function` types then do not narrow.
20465+
const prototypeType = getTypeOfSymbol(prototypeProperty);
20466+
const candidate = !isTypeAny(prototypeType) ? prototypeType : undefined;
20467+
if (!candidate || candidate === globalObjectType || candidate === globalFunctionType) {
20468+
return type;
20469+
}
20470+
20471+
// If the type that is being narrowed is `any` then just return the `candidate` type since every type is a subtype of `any`.
20472+
if (isTypeAny(type)) {
20473+
return candidate;
20474+
}
20475+
20476+
// Filter out types that are not considered to be "constructed by" the `candidate` type.
20477+
return filterType(type, t => isConstructedBy(t, candidate));
20478+
20479+
function isConstructedBy(source: Type, target: Type) {
20480+
// If either the source or target type are a class type then we need to check that they are the same exact type.
20481+
// This is because you may have a class `A` that defines some set of properties, and another class `B`
20482+
// that defines the same set of properties as class `A`, in that case they are structurally the same
20483+
// type, but when you do something like `instanceOfA.constructor === B` it will return false.
20484+
if (source.flags & TypeFlags.Object && getObjectFlags(source) & ObjectFlags.Class ||
20485+
target.flags & TypeFlags.Object && getObjectFlags(target) & ObjectFlags.Class) {
20486+
return source.symbol === target.symbol;
20487+
}
20488+
20489+
// For all other types just check that the `source` type is a subtype of the `target` type.
20490+
return isTypeSubtypeOf(source, target);
20491+
}
20492+
}
20493+
2043520494
function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
2043620495
const left = getReferenceCandidate(expr.left);
2043720496
if (!isMatchingReference(reference, left)) {

src/compiler/utilities.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4426,6 +4426,13 @@ namespace ts {
44264426
return isPropertyAccessExpression(node) && isEntityNameExpression(node.expression);
44274427
}
44284428

4429+
export function isConstructorAccessExpression(expr: Expression): expr is AccessExpression {
4430+
return (
4431+
isPropertyAccessExpression(expr) && idText(expr.name) === "constructor" ||
4432+
isElementAccessExpression(expr) && isStringLiteralLike(expr.argumentExpression) && expr.argumentExpression.text === "constructor"
4433+
);
4434+
}
4435+
44294436
export function tryGetPropertyAccessOrIdentifierToString(expr: Expression): string | undefined {
44304437
if (isPropertyAccessExpression(expr)) {
44314438
const baseStr = tryGetPropertyAccessOrIdentifierToString(expr.expression);
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(66,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
2+
Property 'property1' does not exist on type 'number'.
3+
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(73,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
4+
Property 'property1' does not exist on type 'number'.
5+
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(80,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
6+
Property 'property1' does not exist on type 'number'.
7+
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(87,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
8+
Property 'property1' does not exist on type 'number'.
9+
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(94,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
10+
Property 'property1' does not exist on type 'number'.
11+
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(101,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
12+
Property 'property1' does not exist on type 'number'.
13+
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(108,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
14+
Property 'property1' does not exist on type 'number'.
15+
tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(115,10): error TS2339: Property 'property1' does not exist on type 'number | C1'.
16+
Property 'property1' does not exist on type 'number'.
17+
18+
19+
==== tests/cases/compiler/typeGuardConstructorClassAndNumber.ts (8 errors) ====
20+
// Typical case
21+
class C1 {
22+
property1: string;
23+
}
24+
25+
let var1: C1 | number;
26+
if (var1.constructor == C1) {
27+
var1; // C1
28+
var1.property1; // string
29+
}
30+
else {
31+
var1; // number | C1
32+
}
33+
if (var1["constructor"] == C1) {
34+
var1; // C1
35+
var1.property1; // string
36+
}
37+
else {
38+
var1; // number | C1
39+
}
40+
if (var1.constructor === C1) {
41+
var1; // C1
42+
var1.property1; // string
43+
}
44+
else {
45+
var1; // number | C1
46+
}
47+
if (var1["constructor"] === C1) {
48+
var1; // C1
49+
var1.property1; // string
50+
}
51+
else {
52+
var1; // number | C1
53+
}
54+
if (C1 == var1.constructor) {
55+
var1; // C1
56+
var1.property1; // string
57+
}
58+
else {
59+
var1; // number | C1
60+
}
61+
if (C1 == var1["constructor"]) {
62+
var1; // C1
63+
var1.property1; // string
64+
}
65+
else {
66+
var1; // number | C1
67+
}
68+
if (C1 === var1.constructor) {
69+
var1; // C1
70+
var1.property1; // string
71+
}
72+
else {
73+
var1; // number | C1
74+
}
75+
if (C1 === var1["constructor"]) {
76+
var1; // C1
77+
var1.property1; // string
78+
}
79+
else {
80+
var1; // number | C1
81+
}
82+
83+
if (var1.constructor != C1) {
84+
var1; // C1 | number
85+
var1.property1; // error
86+
~~~~~~~~~
87+
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
88+
!!! error TS2339: Property 'property1' does not exist on type 'number'.
89+
}
90+
else {
91+
var1; // C1
92+
}
93+
if (var1["constructor"] != C1) {
94+
var1; // C1 | number
95+
var1.property1; // error
96+
~~~~~~~~~
97+
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
98+
!!! error TS2339: Property 'property1' does not exist on type 'number'.
99+
}
100+
else {
101+
var1; // C1
102+
}
103+
if (var1.constructor !== C1) {
104+
var1; // C1 | number
105+
var1.property1; // error
106+
~~~~~~~~~
107+
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
108+
!!! error TS2339: Property 'property1' does not exist on type 'number'.
109+
}
110+
else {
111+
var1; // C1
112+
}
113+
if (var1["constructor"] !== C1) {
114+
var1; // C1 | number
115+
var1.property1; // error
116+
~~~~~~~~~
117+
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
118+
!!! error TS2339: Property 'property1' does not exist on type 'number'.
119+
}
120+
else {
121+
var1; // C1
122+
}
123+
if (C1 != var1.constructor) {
124+
var1; // C1 | number
125+
var1.property1; // error
126+
~~~~~~~~~
127+
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
128+
!!! error TS2339: Property 'property1' does not exist on type 'number'.
129+
}
130+
else {
131+
var1; // C1
132+
}
133+
if (C1 != var1["constructor"]) {
134+
var1; // C1 | number
135+
var1.property1; // error
136+
~~~~~~~~~
137+
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
138+
!!! error TS2339: Property 'property1' does not exist on type 'number'.
139+
}
140+
else {
141+
var1; // C1
142+
}
143+
if (C1 !== var1.constructor) {
144+
var1; // C1 | number
145+
var1.property1; // error
146+
~~~~~~~~~
147+
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
148+
!!! error TS2339: Property 'property1' does not exist on type 'number'.
149+
}
150+
else {
151+
var1; // C1
152+
}
153+
if (C1 !== var1["constructor"]) {
154+
var1; // C1 | number
155+
var1.property1; // error
156+
~~~~~~~~~
157+
!!! error TS2339: Property 'property1' does not exist on type 'number | C1'.
158+
!!! error TS2339: Property 'property1' does not exist on type 'number'.
159+
}
160+
else {
161+
var1; // C1
162+
}
163+

0 commit comments

Comments
 (0)