Skip to content

Commit 169e485

Browse files
authored
Perform excess property checking on intersection and union members (microsoft#30853)
* Perform excess property checking on intersection and union members * Allow partial union props to contain the undefined type * Add test case from microsoft#30771 * Un-terse getPossiblePropertiesOfUnionType side-effecting code * Fix bug exposed in RWC * Cache results of getPossiblePropertiesOfUnionType * Fix whitespace
1 parent f617d16 commit 169e485

26 files changed

+1414
-62
lines changed

src/compiler/checker.ts

Lines changed: 56 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7473,6 +7473,25 @@ namespace ts {
74737473
return type.resolvedProperties;
74747474
}
74757475

7476+
function getPossiblePropertiesOfUnionType(type: UnionType): Symbol[] {
7477+
if (type.possiblePropertyCache) {
7478+
return type.possiblePropertyCache.size ? arrayFrom(type.possiblePropertyCache.values()) : emptyArray;
7479+
}
7480+
type.possiblePropertyCache = createSymbolTable();
7481+
for (const t of type.types) {
7482+
for (const p of getPropertiesOfType(t)) {
7483+
if (!type.possiblePropertyCache.has(p.escapedName)) {
7484+
const prop = getUnionOrIntersectionProperty(type, p.escapedName);
7485+
if (prop) {
7486+
type.possiblePropertyCache.set(p.escapedName, prop);
7487+
}
7488+
}
7489+
}
7490+
}
7491+
// We can't simply use the normal property cache here, since that will contain cached apparent type members :(
7492+
return type.possiblePropertyCache.size ? arrayFrom(type.possiblePropertyCache.values()) : emptyArray;
7493+
}
7494+
74767495
function getPropertiesOfType(type: Type): Symbol[] {
74777496
type = getApparentType(type);
74787497
return type.flags & TypeFlags.UnionOrIntersection ?
@@ -7830,7 +7849,7 @@ namespace ts {
78307849
const isUnion = containingType.flags & TypeFlags.Union;
78317850
const excludeModifiers = isUnion ? ModifierFlags.NonPublicAccessibilityModifier : 0;
78327851
// Flags we want to propagate to the result if they exist in all source symbols
7833-
let commonFlags = isUnion ? SymbolFlags.None : SymbolFlags.Optional;
7852+
let optionalFlag = isUnion ? SymbolFlags.None : SymbolFlags.Optional;
78347853
let syntheticFlag = CheckFlags.SyntheticMethod;
78357854
let checkFlags = 0;
78367855
for (const current of containingType.types) {
@@ -7839,7 +7858,12 @@ namespace ts {
78397858
const prop = getPropertyOfType(type, name);
78407859
const modifiers = prop ? getDeclarationModifierFlagsFromSymbol(prop) : 0;
78417860
if (prop && !(modifiers & excludeModifiers)) {
7842-
commonFlags &= prop.flags;
7861+
if (isUnion) {
7862+
optionalFlag |= (prop.flags & SymbolFlags.Optional);
7863+
}
7864+
else {
7865+
optionalFlag &= prop.flags;
7866+
}
78437867
const id = "" + getSymbolId(prop);
78447868
if (!propSet.has(id)) {
78457869
propSet.set(id, prop);
@@ -7857,10 +7881,11 @@ namespace ts {
78577881
const indexInfo = !isLateBoundName(name) && (isNumericLiteralName(name) && getIndexInfoOfType(type, IndexKind.Number) || getIndexInfoOfType(type, IndexKind.String));
78587882
if (indexInfo) {
78597883
checkFlags |= indexInfo.isReadonly ? CheckFlags.Readonly : 0;
7884+
checkFlags |= CheckFlags.WritePartial;
78607885
indexTypes = append(indexTypes, isTupleType(type) ? getRestTypeOfTupleType(type) || undefinedType : indexInfo.type);
78617886
}
78627887
else {
7863-
checkFlags |= CheckFlags.Partial;
7888+
checkFlags |= CheckFlags.ReadPartial;
78647889
}
78657890
}
78667891
}
@@ -7869,7 +7894,7 @@ namespace ts {
78697894
return undefined;
78707895
}
78717896
const props = arrayFrom(propSet.values());
7872-
if (props.length === 1 && !(checkFlags & CheckFlags.Partial) && !indexTypes) {
7897+
if (props.length === 1 && !(checkFlags & CheckFlags.ReadPartial) && !indexTypes) {
78737898
return props[0];
78747899
}
78757900
let declarations: Declaration[] | undefined;
@@ -7900,7 +7925,7 @@ namespace ts {
79007925
propTypes.push(type);
79017926
}
79027927
addRange(propTypes, indexTypes);
7903-
const result = createSymbol(SymbolFlags.Property | commonFlags, name, syntheticFlag | checkFlags);
7928+
const result = createSymbol(SymbolFlags.Property | optionalFlag, name, syntheticFlag | checkFlags);
79047929
result.containingType = containingType;
79057930
if (!hasNonUniformValueDeclaration && firstValueDeclaration) {
79067931
result.valueDeclaration = firstValueDeclaration;
@@ -7937,7 +7962,7 @@ namespace ts {
79377962
function getPropertyOfUnionOrIntersectionType(type: UnionOrIntersectionType, name: __String): Symbol | undefined {
79387963
const property = getUnionOrIntersectionProperty(type, name);
79397964
// We need to filter out partial properties in union types
7940-
return property && !(getCheckFlags(property) & CheckFlags.Partial) ? property : undefined;
7965+
return property && !(getCheckFlags(property) & CheckFlags.ReadPartial) ? property : undefined;
79417966
}
79427967

79437968
/**
@@ -12276,25 +12301,6 @@ namespace ts {
1227612301
return true;
1227712302
}
1227812303

12279-
function isUnionOrIntersectionTypeWithoutNullableConstituents(type: Type): boolean {
12280-
if (!(type.flags & TypeFlags.UnionOrIntersection)) {
12281-
return false;
12282-
}
12283-
// at this point we know that this is union or intersection type possibly with nullable constituents.
12284-
// check if we still will have compound type if we ignore nullable components.
12285-
let seenNonNullable = false;
12286-
for (const t of (<UnionOrIntersectionType>type).types) {
12287-
if (t.flags & TypeFlags.Nullable) {
12288-
continue;
12289-
}
12290-
if (seenNonNullable) {
12291-
return true;
12292-
}
12293-
seenNonNullable = true;
12294-
}
12295-
return false;
12296-
}
12297-
1229812304
/**
1229912305
* Compare two types and return
1230012306
* * Ternary.True if they are related with no assumptions,
@@ -12349,21 +12355,15 @@ namespace ts {
1234912355
isSimpleTypeRelatedTo(source, target, relation, reportErrors ? reportError : undefined)) return Ternary.True;
1235012356

1235112357
const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes);
12352-
if (isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral) {
12358+
const isPerformingExcessPropertyChecks = (isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral);
12359+
if (isPerformingExcessPropertyChecks) {
1235312360
const discriminantType = target.flags & TypeFlags.Union ? findMatchingDiscriminantType(source, target as UnionType) : undefined;
1235412361
if (hasExcessProperties(<FreshObjectLiteralType>source, target, discriminantType, reportErrors)) {
1235512362
if (reportErrors) {
1235612363
reportRelationError(headMessage, source, target);
1235712364
}
1235812365
return Ternary.False;
1235912366
}
12360-
// Above we check for excess properties with respect to the entire target type. When union
12361-
// and intersection types are further deconstructed on the target side, we don't want to
12362-
// make the check again (as it might fail for a partial target type). Therefore we obtain
12363-
// the regular source type and proceed with that.
12364-
if (isUnionOrIntersectionTypeWithoutNullableConstituents(target) && !discriminantType) {
12365-
source = getRegularTypeOfObjectLiteral(source);
12366-
}
1236712367
}
1236812368

1236912369
if (relation !== comparableRelation && !isApparentIntersectionConstituent &&
@@ -12399,11 +12399,24 @@ namespace ts {
1239912399
}
1240012400
else {
1240112401
if (target.flags & TypeFlags.Union) {
12402-
result = typeRelatedToSomeType(source, <UnionType>target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive));
12402+
result = typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), <UnionType>target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive));
12403+
if (result && isPerformingExcessPropertyChecks) {
12404+
// Validate against excess props using the original `source`
12405+
const discriminantType = target.flags & TypeFlags.Union ? findMatchingDiscriminantType(source, target as UnionType) : undefined;
12406+
if (!propertiesRelatedTo(source, discriminantType || target, reportErrors)) {
12407+
return Ternary.False;
12408+
}
12409+
}
1240312410
}
1240412411
else if (target.flags & TypeFlags.Intersection) {
1240512412
isIntersectionConstituent = true; // set here to affect the following trio of checks
12406-
result = typeRelatedToEachType(source, target as IntersectionType, reportErrors);
12413+
result = typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as IntersectionType, reportErrors);
12414+
if (result && isPerformingExcessPropertyChecks) {
12415+
// Validate against excess props using the original `source`
12416+
if (!propertiesRelatedTo(source, target, reportErrors)) {
12417+
return Ternary.False;
12418+
}
12419+
}
1240712420
}
1240812421
else if (source.flags & TypeFlags.Intersection) {
1240912422
// Check to see if any constituents of the intersection are immediately related to the target.
@@ -12506,7 +12519,7 @@ namespace ts {
1250612519
// check excess properties against discriminant type only, not the entire union
1250712520
return hasExcessProperties(source, discriminant, /*discriminant*/ undefined, reportErrors);
1250812521
}
12509-
for (const prop of getPropertiesOfObjectType(source)) {
12522+
for (const prop of getPropertiesOfType(source)) {
1251012523
if (shouldCheckAsExcessProperty(prop, source.symbol) && !isKnownProperty(target, prop.escapedName, isComparingJsxAttributes)) {
1251112524
if (reportErrors) {
1251212525
// Report error in terms of object types in the target as those are the only ones
@@ -13233,7 +13246,9 @@ namespace ts {
1323313246
}
1323413247
}
1323513248
}
13236-
const properties = getPropertiesOfObjectType(target);
13249+
// We only call this for union target types when we're attempting to do excess property checking - in those cases, we want to get _all possible props_
13250+
// from the target union, across all members
13251+
const properties = target.flags & TypeFlags.Union ? getPossiblePropertiesOfUnionType(target as UnionType) : getPropertiesOfType(target);
1323713252
for (const targetProp of properties) {
1323813253
if (!(targetProp.flags & SymbolFlags.Prototype)) {
1323913254
const sourceProp = getPropertyOfType(source, targetProp.escapedName);
@@ -13281,7 +13296,8 @@ namespace ts {
1328113296
}
1328213297
return Ternary.False;
1328313298
}
13284-
const related = isRelatedTo(getTypeOfSymbol(sourceProp), getTypeOfSymbol(targetProp), reportErrors);
13299+
// If the target comes from a partial union prop, allow `undefined` in the target type
13300+
const related = isRelatedTo(getTypeOfSymbol(sourceProp), addOptionality(getTypeOfSymbol(targetProp), !!(getCheckFlags(targetProp) & CheckFlags.Partial)), reportErrors);
1328513301
if (!related) {
1328613302
if (reportErrors) {
1328713303
reportError(Diagnostics.Types_of_property_0_are_incompatible, symbolToString(targetProp));
@@ -14627,9 +14643,9 @@ namespace ts {
1462714643
}
1462814644

1462914645
function* getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean) {
14630-
const properties = target.flags & TypeFlags.Intersection ? getPropertiesOfUnionOrIntersectionType(<IntersectionType>target) : getPropertiesOfObjectType(target);
14646+
const properties = target.flags & TypeFlags.Union ? getPossiblePropertiesOfUnionType(target as UnionType) : getPropertiesOfType(target);
1463114647
for (const targetProp of properties) {
14632-
if (requireOptionalProperties || !(targetProp.flags & SymbolFlags.Optional)) {
14648+
if (requireOptionalProperties || !(targetProp.flags & SymbolFlags.Optional || getCheckFlags(targetProp) & CheckFlags.Partial)) {
1463314649
const sourceProp = getPropertyOfType(source, targetProp.escapedName);
1463414650
if (!sourceProp) {
1463514651
yield targetProp;

src/compiler/types.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3746,19 +3746,21 @@ namespace ts {
37463746
SyntheticProperty = 1 << 1, // Property in union or intersection type
37473747
SyntheticMethod = 1 << 2, // Method in union or intersection type
37483748
Readonly = 1 << 3, // Readonly transient symbol
3749-
Partial = 1 << 4, // Synthetic property present in some but not all constituents
3750-
HasNonUniformType = 1 << 5, // Synthetic property with non-uniform type in constituents
3751-
HasLiteralType = 1 << 6, // Synthetic property with at least one literal type in constituents
3752-
ContainsPublic = 1 << 7, // Synthetic property with public constituent(s)
3753-
ContainsProtected = 1 << 8, // Synthetic property with protected constituent(s)
3754-
ContainsPrivate = 1 << 9, // Synthetic property with private constituent(s)
3755-
ContainsStatic = 1 << 10, // Synthetic property with static constituent(s)
3756-
Late = 1 << 11, // Late-bound symbol for a computed property with a dynamic name
3757-
ReverseMapped = 1 << 12, // Property of reverse-inferred homomorphic mapped type
3758-
OptionalParameter = 1 << 13, // Optional parameter
3759-
RestParameter = 1 << 14, // Rest parameter
3749+
ReadPartial = 1 << 4, // Synthetic property present in some but not all constituents
3750+
WritePartial = 1 << 5, // Synthetic property present in some but only satisfied by an index signature in others
3751+
HasNonUniformType = 1 << 6, // Synthetic property with non-uniform type in constituents
3752+
HasLiteralType = 1 << 7, // Synthetic property with at least one literal type in constituents
3753+
ContainsPublic = 1 << 8, // Synthetic property with public constituent(s)
3754+
ContainsProtected = 1 << 9, // Synthetic property with protected constituent(s)
3755+
ContainsPrivate = 1 << 10, // Synthetic property with private constituent(s)
3756+
ContainsStatic = 1 << 11, // Synthetic property with static constituent(s)
3757+
Late = 1 << 12, // Late-bound symbol for a computed property with a dynamic name
3758+
ReverseMapped = 1 << 13, // Property of reverse-inferred homomorphic mapped type
3759+
OptionalParameter = 1 << 14, // Optional parameter
3760+
RestParameter = 1 << 15, // Rest parameter
37603761
Synthetic = SyntheticProperty | SyntheticMethod,
3761-
Discriminant = HasNonUniformType | HasLiteralType
3762+
Discriminant = HasNonUniformType | HasLiteralType,
3763+
Partial = ReadPartial | WritePartial
37623764
}
37633765

37643766
/* @internal */
@@ -4171,6 +4173,8 @@ namespace ts {
41714173
}
41724174

41734175
export interface UnionType extends UnionOrIntersectionType {
4176+
/* @internal */
4177+
possiblePropertyCache?: SymbolTable; // Cache of _all_ resolved properties less any from aparent members
41744178
}
41754179

41764180
export interface IntersectionType extends UnionOrIntersectionType {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts(21,33): error TS2322: Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
2+
Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
3+
tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts(27,34): error TS2326: Types of property 'icon' are incompatible.
4+
Type '{ props: { INVALID_PROP_NAME: string; ariaLabel: string; }; }' is not assignable to type 'NestedProp<ITestProps>'.
5+
Types of property 'props' are incompatible.
6+
Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
7+
Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
8+
9+
10+
==== tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts (2 errors) ====
11+
interface StatelessComponent<P = {}> {
12+
(props: P & { children?: number }, context?: any): null;
13+
}
14+
15+
const TestComponent: StatelessComponent<TestProps> = (props) => {
16+
return null;
17+
}
18+
19+
interface ITestProps {
20+
ariaLabel?: string;
21+
}
22+
23+
interface NestedProp<TProps> {
24+
props: TProps;
25+
}
26+
27+
interface TestProps {
28+
icon: NestedProp<ITestProps>;
29+
}
30+
31+
TestComponent({icon: { props: { INVALID_PROP_NAME: 'share', ariaLabel: 'test label' } }});
32+
~~~~~~~~~~~~~~~~~~~~~~~~~~
33+
!!! error TS2322: Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
34+
!!! error TS2322: Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
35+
!!! related TS6500 tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts:14:3: The expected type comes from property 'props' which is declared here on type 'NestedProp<ITestProps>'
36+
37+
const TestComponent2: StatelessComponent<TestProps | {props2: {x: number}}> = (props) => {
38+
return null;
39+
}
40+
41+
TestComponent2({icon: { props: { INVALID_PROP_NAME: 'share', ariaLabel: 'test label' } }});
42+
~~~~~~~~~~~~~~~~~~~~~~~~~~
43+
!!! error TS2326: Types of property 'icon' are incompatible.
44+
!!! error TS2326: Type '{ props: { INVALID_PROP_NAME: string; ariaLabel: string; }; }' is not assignable to type 'NestedProp<ITestProps>'.
45+
!!! error TS2326: Types of property 'props' are incompatible.
46+
!!! error TS2326: Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
47+
!!! error TS2326: Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
48+

0 commit comments

Comments
 (0)