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
in
operator typeguard can widen types
#46403
Changes from 58 commits
5a41201
775fd9c
b974add
49b574b
734f4c2
b6cbb3e
d6cabee
48b27e7
5936e27
02ff40a
6ec0edc
79e81be
c2fc19e
a5d17f3
540545a
b6f36cd
13b276c
84f5eeb
f2ee389
758d8df
d10b8d3
e245b88
c79a1fd
f8586b8
3e70f10
5def93a
26a982b
e93b2dc
7d49f33
728a8a3
47d206e
a29c5a5
5bcc2e6
78a50e9
f7bcee0
2015750
08dcd94
9da464b
94b9311
a47d9b2
5594882
b9f6aaf
4b4e63b
fbd0454
6b7b0b1
225f5a1
1f5c6c5
c7641f1
704ae12
83fda77
d739a5a
b75b129
624f586
e53fb8a
8bc9ef4
5eb7dcd
6010fff
caab233
762e56b
b16a4a1
ae755aa
b1fadcd
46e7bc6
efdcc0d
0bbe651
58e9420
aca5344
9c681b6
acf47c8
7293a62
8c3c2ac
dfe51a4
5c32123
d4be592
2f997b5
ed9eea3
02c8b2d
75dc33e
1b82d01
f5555df
ca14f27
13d2150
c891690
2b60aa8
30f89ae
ec41cbb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12118,8 +12118,11 @@ namespace ts { | |
return property; | ||
} | ||
|
||
function getPropertyOfUnionOrIntersectionType(type: UnionOrIntersectionType, name: __String, skipObjectFunctionPropertyAugment?: boolean): Symbol | undefined { | ||
function getPropertyOfUnionOrIntersectionType(type: UnionOrIntersectionType, name: __String, skipObjectFunctionPropertyAugment?: boolean, includePartialProperties?: boolean): Symbol | undefined { | ||
const property = getUnionOrIntersectionProperty(type, name, skipObjectFunctionPropertyAugment); | ||
if (includePartialProperties) { | ||
return property; | ||
} | ||
// We need to filter out partial properties in union types | ||
return property && !(getCheckFlags(property) & CheckFlags.ReadPartial) ? property : undefined; | ||
} | ||
|
@@ -12197,7 +12200,7 @@ namespace ts { | |
* @param type a type to look up property from | ||
* @param name a name of property to look up in a given type | ||
*/ | ||
function getPropertyOfType(type: Type, name: __String, skipObjectFunctionPropertyAugment?: boolean): Symbol | undefined { | ||
function getPropertyOfType(type: Type, name: __String, skipObjectFunctionPropertyAugment?: boolean, includePartialProperties?: boolean): Symbol | undefined { | ||
type = getReducedApparentType(type); | ||
if (type.flags & TypeFlags.Object) { | ||
const resolved = resolveStructuredTypeMembers(type as ObjectType); | ||
|
@@ -12219,7 +12222,7 @@ namespace ts { | |
return getPropertyOfObjectType(globalObjectType, name); | ||
} | ||
if (type.flags & TypeFlags.UnionOrIntersection) { | ||
return getPropertyOfUnionOrIntersectionType(type as UnionOrIntersectionType, name, skipObjectFunctionPropertyAugment); | ||
return getPropertyOfUnionOrIntersectionType(type as UnionOrIntersectionType, name, skipObjectFunctionPropertyAugment, includePartialProperties); | ||
} | ||
return undefined; | ||
} | ||
|
@@ -14346,6 +14349,32 @@ namespace ts { | |
return type; | ||
} | ||
|
||
// This function assumes the constituent type list is sorted and deduplicated. | ||
function getIntersectionTypeFromSortedList(types: Type[], objectFlags: ObjectFlags, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type { | ||
if (types.length === 0) { | ||
return neverType; | ||
} | ||
if (types.length === 1) { | ||
return types[0]; | ||
} | ||
const id = getTypeListId(types) + getAliasId(aliasSymbol, aliasTypeArguments); | ||
const existingType = intersectionTypes.get(id); | ||
if (existingType) { | ||
return existingType; | ||
} | ||
const type = createType(TypeFlags.Intersection) as IntersectionType; | ||
type.objectFlags = objectFlags | getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable); | ||
type.types = types; | ||
type.aliasSymbol = aliasSymbol; | ||
type.aliasTypeArguments = aliasTypeArguments; | ||
if (types.length === 2 && types[0].flags & TypeFlags.BooleanLiteral && types[1].flags & TypeFlags.BooleanLiteral) { | ||
type.flags |= TypeFlags.Boolean; | ||
(type as IntersectionType & IntrinsicType).intrinsicName = "boolean"; | ||
} | ||
intersectionTypes.set(id, type); | ||
return type; | ||
} | ||
|
||
function getTypeFromUnionTypeNode(node: UnionTypeNode): Type { | ||
const links = getNodeLinks(node); | ||
if (!links.resolvedType) { | ||
|
@@ -23074,14 +23103,14 @@ namespace ts { | |
return type.flags & TypeFlags.UnionOrIntersection ? every((type as UnionOrIntersectionType).types, f) : f(type); | ||
} | ||
|
||
function filterType(type: Type, f: (t: Type) => boolean): Type { | ||
if (type.flags & TypeFlags.Union) { | ||
const types = (type as UnionType).types; | ||
const filtered = filter(types, f); | ||
if (filtered === types) { | ||
return type; | ||
} | ||
const origin = (type as UnionType).origin; | ||
function filterUnionOrIntersectionType(type: UnionOrIntersectionType, f: (t: Type) => boolean): Type { | ||
const types = type.types; | ||
const filtered = filter(types, f); | ||
if (filtered === types) { | ||
return type; | ||
} | ||
if (isUnionType(type)) { | ||
const origin = type.origin; | ||
let newOrigin: Type | undefined; | ||
if (origin && origin.flags & TypeFlags.Union) { | ||
// If the origin type is a (denormalized) union type, filter its non-union constituents. If that ends | ||
|
@@ -23098,7 +23127,14 @@ namespace ts { | |
newOrigin = createOriginUnionOrIntersectionType(TypeFlags.Union, originFiltered); | ||
} | ||
} | ||
return getUnionTypeFromSortedList(filtered, (type as UnionType).objectFlags, /*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined, newOrigin); | ||
return getUnionTypeFromSortedList(filtered, type.objectFlags, /*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined, newOrigin); | ||
} | ||
return getIntersectionTypeFromSortedList(filtered, type.objectFlags); | ||
} | ||
|
||
function filterType(type: Type, f: (t: Type) => boolean): Type { | ||
if (isUnionType(type)) { | ||
return filterUnionOrIntersectionType(type, f); | ||
} | ||
return type.flags & TypeFlags.Never || f(type) ? type : neverType; | ||
} | ||
|
@@ -24062,12 +24098,61 @@ namespace ts { | |
return getApplicableIndexInfoForName(type, propName) ? true : !assumeTrue; | ||
} | ||
|
||
function narrowByInKeyword(type: Type, name: __String, assumeTrue: boolean) { | ||
if (type.flags & TypeFlags.Union | ||
|| type.flags & TypeFlags.Object && declaredType !== type | ||
|| isThisTypeParameter(type) | ||
|| type.flags & TypeFlags.Intersection && every((type as IntersectionType).types, t => t.symbol !== globalThisSymbol)) { | ||
return filterType(type, t => isTypePresencePossible(t, name, assumeTrue)); | ||
function widenTypeWithSymbol(type: Type, newSymbol: Symbol): Type { | ||
// If type is this/any/unknown, it cannot be widened. | ||
if ((type.flags & TypeFlags.AnyOrUnknown) || isThisTypeParameter(type)) { | ||
return type; | ||
} | ||
// If type is anonymous object, add the symbol directly | ||
marekdedic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (isObjectType(type) && type.objectFlags & ObjectFlags.Anonymous) { | ||
return widenObjectType(type, newSymbol); | ||
} | ||
// If type is intersection, add the symbol to the first anonymous object component of the intersection | ||
if (isIntersectionType(type)) { | ||
marekdedic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const objectSubtype = type.types.find(t => getObjectFlags(t) & ObjectFlags.Anonymous) as ObjectType | undefined; | ||
if (objectSubtype) { | ||
const restOfIntersection = filterUnionOrIntersectionType(type, t => t !== objectSubtype); | ||
return createIntersectionType([restOfIntersection, widenObjectType(objectSubtype, newSymbol)]); | ||
} | ||
} | ||
|
||
// Otherwise, just add the new object type as an intersection | ||
const newTypeWithSymbol = widenObjectType(createAnonymousType(undefined, createSymbolTable(), emptyArray, emptyArray, emptyArray), newSymbol); | ||
return createIntersectionType([type, newTypeWithSymbol]); | ||
|
||
function widenObjectType(type: ObjectType, newSymbol: Symbol): Type { | ||
const members = createSymbolTable(); | ||
if (type.members !== undefined) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi, @weswigham There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Don't merge the types :) Instead, just make an anonymous type with a single member - the new one - and use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Essentially everything you're doing by hand here in the last 3/4ths of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi, - return widenTypeWithSymbol(type, addSymbol);
+ const members = createSymbolTable([addSymbol]);
+ const addType = createAnonymousType(undefined, members, emptyArray, emptyArray, emptyArray);
+ return getSpreadType(type, addType, undefined, getObjectFlags(type), false); It's a really neat solution that removes the entire See it in action here |
||
mergeSymbolTable(members, type.members); | ||
} | ||
members.set(newSymbol.escapedName, newSymbol); | ||
return createAnonymousType(undefined, members, type.callSignatures ?? emptyArray, type.constructSignatures ?? emptyArray, type.indexInfos ?? emptyArray); | ||
} | ||
} | ||
|
||
function narrowOrWidenTypeByInKeyword(type: Type, name: __String, assumeTrue: boolean) { | ||
// If type contains global this, don't touch it | ||
if (type.symbol === globalThisSymbol | ||
|| isUnionOrIntersectionType(type) && filterUnionOrIntersectionType(type, t => t.symbol === globalThisSymbol) !== neverType | ||
) { | ||
return type; | ||
} | ||
const someDirectSubtypeContainsProp = getPropertyOfType(type, name, /* skipObjectFunctionPropertyAugment */ false, /* includePartialProperties */ true); | ||
marekdedic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (someDirectSubtypeContainsProp) { | ||
// If union, filter out all components not containing the property | ||
// Otherwise, either return the type or never | ||
if (type.flags & (TypeFlags.Object | TypeFlags.UnionOrIntersection) | ||
|| isThisTypeParameter(type) | ||
) { | ||
return filterType(type, t => isTypePresencePossible(t, name, assumeTrue)); | ||
return isTypePresencePossible(type, name, assumeTrue) ? type : neverType; | ||
} | ||
} | ||
// only widen property when the type does not contain string-index/name in any of the constituents. | ||
if (assumeTrue && !someDirectSubtypeContainsProp && !getIndexInfoOfType(type, stringType)) { | ||
const addSymbol = createSymbol(SymbolFlags.Property, name); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should cache these symbols (and the resulting properties and single-property-types) globally within the checker. Object types aren't structurally cached, so it needs to be done explicitly when the type is constructed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, will do that once the discussion about this implementation is resolved |
||
addSymbol.type = unknownType; | ||
return widenTypeWithSymbol(type, addSymbol); | ||
} | ||
return type; | ||
} | ||
|
@@ -24136,7 +24221,7 @@ namespace ts { | |
return getTypeWithFacts(type, assumeTrue ? TypeFacts.NEUndefined : TypeFacts.EQUndefined); | ||
} | ||
if (isMatchingReference(reference, target)) { | ||
return narrowByInKeyword(type, name, assumeTrue); | ||
return narrowOrWidenTypeByInKeyword(type, name, assumeTrue); | ||
} | ||
} | ||
break; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/* @internal */ | ||
namespace ts { | ||
marekdedic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
export function isUnionOrIntersectionType(type: Type): type is UnionOrIntersectionType { | ||
return !!(type.flags & TypeFlags.UnionOrIntersection); | ||
} | ||
|
||
export function isUnionType(type: Type): type is UnionType { | ||
return !!(type.flags & TypeFlags.Union); | ||
} | ||
|
||
export function isIntersectionType(type: Type): type is IntersectionType { | ||
return !!(type.flags & TypeFlags.Intersection); | ||
} | ||
|
||
export function isObjectType(type: Type): type is ObjectType { | ||
return !!(type.flags & TypeFlags.Object); | ||
} | ||
} |
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.
I'd probably make a separate helper for filtering intersections - if you even still need it after swapping to using
getSpreadType
- rather then repurposingfilterType
. most offilterType
's callers definitely don't want to filter intersection constituents.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.
Ok, hopefully, the change won't be needed altogether with the introduction of
getSpreadType
(I use it in one other place, but it isn't necessary I think)