Skip to content
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

Fix 21732: "in" operator could widden type #39746

Closed
95 changes: 91 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21459,12 +21459,99 @@ namespace ts {
return !assumeTrue;
}

function narrowByInKeyword(type: Type, literal: LiteralExpression, assumeTrue: boolean) {
if (type.flags & (TypeFlags.Union | TypeFlags.Object) || isThisTypeParameter(type)) {
const propName = escapeLeadingUnderscores(literal.text);
function widdenTypeWithSymbol(type: Type, newSymbol: Symbol): Type {
// If type is this/any/unknown, it could not be widden.
if ((type.flags & TypeFlags.AnyOrUnknown) || isThisTypeParameter(type)) {
return type;
}
const propName = newSymbol.escapedName;
// if type is intersection, we might have added type into it, and we just need to add into this type again rather than a new one.
// else add a new anonymous object type which contains the type and widden the origional type with it.
if (isIntersectionType(type)) {
// try to get the first Anonymous Object type to add new type to it.
const firstAnonymousObjectType: Type | undefined = type.types.find(t => isObjectType(t) && t.objectFlags & ObjectFlags.Anonymous);
if (firstAnonymousObjectType && isObjectType(firstAnonymousObjectType)) {
const members = createSymbolTable();
members.set(propName, newSymbol);
if (firstAnonymousObjectType.members) {
mergeSymbolTable(members, firstAnonymousObjectType.members);
}
firstAnonymousObjectType.members = members;
firstAnonymousObjectType.properties = getNamedMembers(members);
}
else {
const members = createSymbolTable();
members.set(propName, newSymbol);
const newObjType = createAnonymousType(undefined, members, emptyArray, emptyArray, undefined, undefined);
return createIntersectionType([type, newObjType]);
}
}
else {
const members = createSymbolTable();
members.set(propName, newSymbol);
const newObjType = createAnonymousType(undefined, members, emptyArray, emptyArray, undefined, undefined);
// if `type` is never, just return the new anonymous object type.
if (type.flags & TypeFlags.Never) {
return newObjType;
}
return createIntersectionType([type, newObjType]);
}
return type;

// I would be very glad to create a helper file like `nodeTests.ts` if feedback positive review.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, what about a helper file for Type to narrow their type?

function isIntersectionType(type: Type): type is IntersectionType {
return !!(type.flags & TypeFlags.Intersection);
}
function isObjectType(type: Type): type is ObjectType {
return !!(type.flags & TypeFlags.Object);
}
}

function narrowOrWiddenTypeByInKeyword(type: Type, literal: LiteralExpression, assumeTrue: boolean) {
const propName = escapeLeadingUnderscores(literal.text);
const addSymbol = createSymbol(SymbolFlags.Property, propName);
addSymbol.type = unknownType;

if ((type.flags & (TypeFlags.Union | TypeFlags.Object) || isThisTypeParameter(type)) && isSomeDirectSubtypeContainsPropName(type, propName)) {
return filterType(type, t => isTypePresencePossible(t, propName, assumeTrue));
}
// only widden property when the type does not contain string-index/propName in any of the constituents.
else if (assumeTrue && !isSomeDirectSubtypeContainsPropName(type, propName) && !getIndexInfoOfType(type, IndexKind.String)) {
return widdenTypeWithSymbol(type, addSymbol);
}
return type;

// This function is almost like function `getPropertyOfType`, except when type.flags contains `UnionOrIntersection`
// it would return the property rather than undefiend even when property is partial.
function isSomeDirectSubtypeContainsPropName(type1: Type, name: __String) {
let prop;
const type = getReducedApparentType(type1);
if (type.flags & TypeFlags.Object) {
const resolved = resolveStructuredTypeMembers(<ObjectType>type);
const symbol = resolved.members.get(name);
if (symbol && symbolIsValue(symbol)) {
prop = symbol;
}
const functionType = resolved === anyFunctionType ? globalFunctionType :
resolved.callSignatures.length ? globalCallableFunctionType :
resolved.constructSignatures.length ? globalNewableFunctionType :
undefined;
if (functionType) {
const symbol = getPropertyOfObjectType(functionType, name);
if (symbol) {
prop = symbol;
}
}
return getPropertyOfObjectType(globalObjectType, name);
}
if (type.flags & TypeFlags.UnionOrIntersection) {
prop = getUnionOrIntersectionProperty(<UnionOrIntersectionType>type, name);
}
if (prop) {
return true;
}
return false;
}
}

function narrowTypeByBinaryExpression(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
Expand Down Expand Up @@ -21519,7 +21606,7 @@ namespace ts {
case SyntaxKind.InKeyword:
const target = getReferenceCandidate(expr.right);
if (isStringLiteralLike(expr.left) && isMatchingReference(reference, target)) {
return narrowByInKeyword(type, expr.left, assumeTrue);
return narrowOrWiddenTypeByInKeyword(type, expr.left, assumeTrue);
}
break;
case SyntaxKind.CommaToken:
Expand Down
10 changes: 5 additions & 5 deletions tests/baselines/reference/conditionalTypeDoesntSpinForever.types
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,12 @@ export enum PubSubRecordIsStoredInRedisAsA {
>buildPubSubRecordType(Object.assign({}, soFar, {identifier: instance as TYPE}) as SO_FAR & {identifier: TYPE}) : BuildPubSubRecordType<SO_FAR & { identifier: TYPE; }>
>buildPubSubRecordType : <SO_FAR>(soFar: SO_FAR) => BuildPubSubRecordType<SO_FAR>
>Object.assign({}, soFar, {identifier: instance as TYPE}) as SO_FAR & {identifier: TYPE} : SO_FAR & { identifier: TYPE; }
>Object.assign({}, soFar, {identifier: instance as TYPE}) : SO_FAR & { identifier: TYPE; }
>Object.assign({}, soFar, {identifier: instance as TYPE}) : SO_FAR & { record: unknown; } & { identifier: TYPE; }
>Object.assign : { <T, U>(target: T, source: U): T & U; <T, U, V>(target: T, source1: U, source2: V): T & U & V; <T, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W; (target: object, ...sources: any[]): any; }
>Object : ObjectConstructor
>assign : { <T, U>(target: T, source: U): T & U; <T, U, V>(target: T, source1: U, source2: V): T & U & V; <T, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W; (target: object, ...sources: any[]): any; }
>{} : {}
>soFar : SO_FAR
>soFar : SO_FAR & { record: unknown; }
>{identifier: instance as TYPE} : { identifier: TYPE; }
>identifier : TYPE
>instance as TYPE : TYPE
Expand Down Expand Up @@ -389,13 +389,13 @@ export enum PubSubRecordIsStoredInRedisAsA {
>soFar : SO_FAR
>"object" in soFar : boolean
>"object" : "object"
>soFar : SO_FAR
>soFar : SO_FAR & { identifier: unknown; }
>"maxMsToWaitBeforePublishing" in soFar : boolean
>"maxMsToWaitBeforePublishing" : "maxMsToWaitBeforePublishing"
>soFar : SO_FAR
>soFar : SO_FAR & { object: unknown; identifier: unknown; }
>"PubSubRecordIsStoredInRedisAsA" in soFar : boolean
>"PubSubRecordIsStoredInRedisAsA" : "PubSubRecordIsStoredInRedisAsA"
>soFar : SO_FAR
>soFar : SO_FAR & { maxMsToWaitBeforePublishing: unknown; object: unknown; identifier: unknown; }
>{} : {}
>{ type: soFar, fields: () => new Set(Object.keys(soFar) as (keyof SO_FAR)[]), hasField: (fieldName: string | number | symbol) => fieldName in soFar } : { type: SO_FAR; fields: () => Set<keyof SO_FAR>; hasField: (fieldName: string | number | symbol) => boolean; }

Expand Down
5 changes: 1 addition & 4 deletions tests/baselines/reference/fixSignatureCaching.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ tests/cases/conformance/fixSignatureCaching.ts(284,10): error TS2339: Property '
tests/cases/conformance/fixSignatureCaching.ts(293,10): error TS2339: Property 'FALLBACK_PHONE' does not exist on type '{}'.
tests/cases/conformance/fixSignatureCaching.ts(294,10): error TS2339: Property 'FALLBACK_TABLET' does not exist on type '{}'.
tests/cases/conformance/fixSignatureCaching.ts(295,10): error TS2339: Property 'FALLBACK_MOBILE' does not exist on type '{}'.
tests/cases/conformance/fixSignatureCaching.ts(301,17): error TS2339: Property 'isArray' does not exist on type 'never'.
tests/cases/conformance/fixSignatureCaching.ts(330,74): error TS2339: Property 'mobileDetectRules' does not exist on type '{}'.
tests/cases/conformance/fixSignatureCaching.ts(369,10): error TS2339: Property 'findMatch' does not exist on type '{}'.
tests/cases/conformance/fixSignatureCaching.ts(387,10): error TS2339: Property 'findMatches' does not exist on type '{}'.
Expand Down Expand Up @@ -59,7 +58,7 @@ tests/cases/conformance/fixSignatureCaching.ts(981,16): error TS2304: Cannot fin
tests/cases/conformance/fixSignatureCaching.ts(983,44): error TS2339: Property 'MobileDetect' does not exist on type 'Window & typeof globalThis'.


==== tests/cases/conformance/fixSignatureCaching.ts (59 errors) ====
==== tests/cases/conformance/fixSignatureCaching.ts (58 errors) ====
// Repro from #10697

(function (define, undefined) {
Expand Down Expand Up @@ -371,8 +370,6 @@ tests/cases/conformance/fixSignatureCaching.ts(983,44): error TS2339: Property '
isArray = 'isArray' in Array
? function (value) { return Object.prototype.toString.call(value) === '[object Array]'; }
: Array.isArray;
~~~~~~~
!!! error TS2339: Property 'isArray' does not exist on type 'never'.

function equalIC(a, b) {
return a != null && b != null && a.toLowerCase() === b.toLowerCase();
Expand Down
2 changes: 2 additions & 0 deletions tests/baselines/reference/fixSignatureCaching.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,9 @@ define(function () {
>value : Symbol(value, Decl(fixSignatureCaching.ts, 299, 20))

: Array.isArray;
>Array.isArray : Symbol(ArrayConstructor.isArray, Decl(lib.es5.d.ts, --, --))
>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>isArray : Symbol(ArrayConstructor.isArray, Decl(lib.es5.d.ts, --, --))

function equalIC(a, b) {
>equalIC : Symbol(equalIC, Decl(fixSignatureCaching.ts, 300, 24))
Expand Down
12 changes: 6 additions & 6 deletions tests/baselines/reference/fixSignatureCaching.types
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,7 @@ define(function () {

Array.isArray : function (value) { return Object.prototype.toString.call(value) === '[object Array]'; };
>Array.isArray : (arg: any) => arg is any[]
>Array : ArrayConstructor
>Array : ArrayConstructor & { isArray: unknown; }
>isArray : (arg: any) => arg is any[]
>function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } : (value: any) => boolean
>value : any
Expand All @@ -1127,9 +1127,9 @@ define(function () {
>'[object Array]' : "[object Array]"

isArray = 'isArray' in Array
>isArray = 'isArray' in Array ? function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } : Array.isArray : any
>isArray = 'isArray' in Array ? function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } : Array.isArray : (value: any) => boolean
>isArray : any
>'isArray' in Array ? function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } : Array.isArray : any
>'isArray' in Array ? function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } : Array.isArray : (value: any) => boolean
>'isArray' in Array : boolean
>'isArray' : "isArray"
>Array : ArrayConstructor
Expand All @@ -1150,9 +1150,9 @@ define(function () {
>'[object Array]' : "[object Array]"

: Array.isArray;
>Array.isArray : any
>Array : never
>isArray : any
>Array.isArray : (arg: any) => arg is any[]
>Array : ArrayConstructor
>isArray : (arg: any) => arg is any[]

function equalIC(a, b) {
>equalIC : (a: any, b: any) => boolean
Expand Down
13 changes: 5 additions & 8 deletions tests/baselines/reference/inKeywordTypeguard.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ tests/cases/compiler/inKeywordTypeguard.ts(16,11): error TS2339: Property 'a' do
tests/cases/compiler/inKeywordTypeguard.ts(27,11): error TS2339: Property 'b' does not exist on type 'AWithOptionalProp | BWithOptionalProp'.
Property 'b' does not exist on type 'AWithOptionalProp'.
tests/cases/compiler/inKeywordTypeguard.ts(42,11): error TS2339: Property 'b' does not exist on type 'AWithMethod'.
tests/cases/compiler/inKeywordTypeguard.ts(49,11): error TS2339: Property 'a' does not exist on type 'never'.
tests/cases/compiler/inKeywordTypeguard.ts(50,11): error TS2339: Property 'b' does not exist on type 'never'.
tests/cases/compiler/inKeywordTypeguard.ts(49,11): error TS2339: Property 'a' does not exist on type '(AWithMethod | BWithMethod) & { c: unknown; }'.
tests/cases/compiler/inKeywordTypeguard.ts(50,11): error TS2339: Property 'b' does not exist on type '(AWithMethod | BWithMethod) & { c: unknown; }'.
tests/cases/compiler/inKeywordTypeguard.ts(52,11): error TS2339: Property 'a' does not exist on type 'AWithMethod | BWithMethod'.
Property 'a' does not exist on type 'BWithMethod'.
tests/cases/compiler/inKeywordTypeguard.ts(53,11): error TS2339: Property 'b' does not exist on type 'AWithMethod | BWithMethod'.
Expand All @@ -18,11 +18,10 @@ tests/cases/compiler/inKeywordTypeguard.ts(72,32): error TS2339: Property 'b' do
tests/cases/compiler/inKeywordTypeguard.ts(74,32): error TS2339: Property 'a' does not exist on type 'B'.
tests/cases/compiler/inKeywordTypeguard.ts(82,39): error TS2339: Property 'b' does not exist on type 'A'.
tests/cases/compiler/inKeywordTypeguard.ts(84,39): error TS2339: Property 'a' does not exist on type 'B'.
tests/cases/compiler/inKeywordTypeguard.ts(94,26): error TS2339: Property 'a' does not exist on type 'never'.
tests/cases/compiler/inKeywordTypeguard.ts(103,13): error TS2322: Type '{ a: string; } & { b: string; }' is not assignable to type 'never'.


==== tests/cases/compiler/inKeywordTypeguard.ts (18 errors) ====
==== tests/cases/compiler/inKeywordTypeguard.ts (17 errors) ====
class A { a: string; }
class B { b: string; }

Expand Down Expand Up @@ -86,10 +85,10 @@ tests/cases/compiler/inKeywordTypeguard.ts(103,13): error TS2322: Type '{ a: str
if ("c" in x) {
x.a();
~
!!! error TS2339: Property 'a' does not exist on type 'never'.
!!! error TS2339: Property 'a' does not exist on type '(AWithMethod | BWithMethod) & { c: unknown; }'.
x.b();
~
!!! error TS2339: Property 'b' does not exist on type 'never'.
!!! error TS2339: Property 'b' does not exist on type '(AWithMethod | BWithMethod) & { c: unknown; }'.
} else {
x.a();
~
Expand Down Expand Up @@ -153,8 +152,6 @@ tests/cases/compiler/inKeywordTypeguard.ts(103,13): error TS2322: Type '{ a: str
if ("a" in this) {
} else {
let y = this.a;
~
!!! error TS2339: Property 'a' does not exist on type 'never'.
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions tests/baselines/reference/inKeywordTypeguard.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ class UnreachableCodeDetection {
} else {
let y = this.a;
>y : Symbol(y, Decl(inKeywordTypeguard.ts, 93, 15))
>this.a : Symbol(UnreachableCodeDetection.a, Decl(inKeywordTypeguard.ts, 88, 32))
>this : Symbol(UnreachableCodeDetection, Decl(inKeywordTypeguard.ts, 86, 1))
>a : Symbol(UnreachableCodeDetection.a, Decl(inKeywordTypeguard.ts, 88, 32))
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions tests/baselines/reference/inKeywordTypeguard.types
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,13 @@ function negativeTestClassesWithMemberMissingInBothClasses(x: AWithMethod | BWit
x.a();
>x.a() : any
>x.a : any
>x : never
>x : (AWithMethod | BWithMethod) & { c: unknown; }
>a : any

x.b();
>x.b() : any
>x.b : any
>x : never
>x : (AWithMethod | BWithMethod) & { c: unknown; }
>b : any

} else {
Expand Down Expand Up @@ -290,10 +290,10 @@ class UnreachableCodeDetection {

} else {
let y = this.a;
>y : any
>this.a : any
>this : never
>a : any
>y : string
>this.a : string
>this : this
>a : string
}
}
}
Expand Down