Skip to content

Commit b3f9bc0

Browse files
committed
Perform excess property checking on intersection and union members
1 parent ff95909 commit b3f9bc0

13 files changed

+994
-61
lines changed

src/compiler/checker.ts

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

7476+
function getPossiblePropertiesOfUnionType(type: UnionType): Symbol[] {
7477+
// The following is for effects - getUnionOrIntersectionProperty will cache all the possible union properties into `type`
7478+
void map(flatMap(type.types, getPropertiesOfType), p => getUnionOrIntersectionProperty(type, p.escapedName));
7479+
// And we can then (uniquely) fetch them out of the cache, instead of as a result of the above call.
7480+
return !type.propertyCache ? emptyArray : arrayFrom(type.propertyCache.values());
7481+
}
7482+
74767483
function getPropertiesOfType(type: Type): Symbol[] {
74777484
type = getApparentType(type);
74787485
return type.flags & TypeFlags.UnionOrIntersection ?
@@ -7832,7 +7839,7 @@ namespace ts {
78327839
const isUnion = containingType.flags & TypeFlags.Union;
78337840
const excludeModifiers = isUnion ? ModifierFlags.NonPublicAccessibilityModifier : 0;
78347841
// Flags we want to propagate to the result if they exist in all source symbols
7835-
let commonFlags = isUnion ? SymbolFlags.None : SymbolFlags.Optional;
7842+
let optionalFlag = isUnion ? SymbolFlags.None : SymbolFlags.Optional;
78367843
let syntheticFlag = CheckFlags.SyntheticMethod;
78377844
let checkFlags = 0;
78387845
for (const current of containingType.types) {
@@ -7841,7 +7848,12 @@ namespace ts {
78417848
const prop = getPropertyOfType(type, name);
78427849
const modifiers = prop ? getDeclarationModifierFlagsFromSymbol(prop) : 0;
78437850
if (prop && !(modifiers & excludeModifiers)) {
7844-
commonFlags &= prop.flags;
7851+
if (isUnion) {
7852+
optionalFlag |= (prop.flags & SymbolFlags.Optional);
7853+
}
7854+
else {
7855+
optionalFlag &= prop.flags;
7856+
}
78457857
const id = "" + getSymbolId(prop);
78467858
if (!propSet.has(id)) {
78477859
propSet.set(id, prop);
@@ -7859,10 +7871,11 @@ namespace ts {
78597871
const indexInfo = !isLateBoundName(name) && (isNumericLiteralName(name) && getIndexInfoOfType(type, IndexKind.Number) || getIndexInfoOfType(type, IndexKind.String));
78607872
if (indexInfo) {
78617873
checkFlags |= indexInfo.isReadonly ? CheckFlags.Readonly : 0;
7874+
checkFlags |= CheckFlags.WritePartial;
78627875
indexTypes = append(indexTypes, isTupleType(type) ? getRestTypeOfTupleType(type) || undefinedType : indexInfo.type);
78637876
}
78647877
else {
7865-
checkFlags |= CheckFlags.Partial;
7878+
checkFlags |= CheckFlags.ReadPartial;
78667879
}
78677880
}
78687881
}
@@ -7871,7 +7884,7 @@ namespace ts {
78717884
return undefined;
78727885
}
78737886
const props = arrayFrom(propSet.values());
7874-
if (props.length === 1 && !(checkFlags & CheckFlags.Partial) && !indexTypes) {
7887+
if (props.length === 1 && !(checkFlags & CheckFlags.ReadPartial) && !indexTypes) {
78757888
return props[0];
78767889
}
78777890
let declarations: Declaration[] | undefined;
@@ -7902,7 +7915,7 @@ namespace ts {
79027915
propTypes.push(type);
79037916
}
79047917
addRange(propTypes, indexTypes);
7905-
const result = createSymbol(SymbolFlags.Property | commonFlags, name, syntheticFlag | checkFlags);
7918+
const result = createSymbol(SymbolFlags.Property | optionalFlag, name, syntheticFlag | checkFlags);
79067919
result.containingType = containingType;
79077920
if (!hasNonUniformValueDeclaration && firstValueDeclaration) {
79087921
result.valueDeclaration = firstValueDeclaration;
@@ -7939,7 +7952,7 @@ namespace ts {
79397952
function getPropertyOfUnionOrIntersectionType(type: UnionOrIntersectionType, name: __String): Symbol | undefined {
79407953
const property = getUnionOrIntersectionProperty(type, name);
79417954
// We need to filter out partial properties in union types
7942-
return property && !(getCheckFlags(property) & CheckFlags.Partial) ? property : undefined;
7955+
return property && !(getCheckFlags(property) & CheckFlags.ReadPartial) ? property : undefined;
79437956
}
79447957

79457958
/**
@@ -12204,25 +12217,6 @@ namespace ts {
1220412217
}
1220512218
}
1220612219

12207-
function isUnionOrIntersectionTypeWithoutNullableConstituents(type: Type): boolean {
12208-
if (!(type.flags & TypeFlags.UnionOrIntersection)) {
12209-
return false;
12210-
}
12211-
// at this point we know that this is union or intersection type possibly with nullable constituents.
12212-
// check if we still will have compound type if we ignore nullable components.
12213-
let seenNonNullable = false;
12214-
for (const t of (<UnionOrIntersectionType>type).types) {
12215-
if (t.flags & TypeFlags.Nullable) {
12216-
continue;
12217-
}
12218-
if (seenNonNullable) {
12219-
return true;
12220-
}
12221-
seenNonNullable = true;
12222-
}
12223-
return false;
12224-
}
12225-
1222612220
/**
1222712221
* Compare two types and return
1222812222
* * Ternary.True if they are related with no assumptions,
@@ -12277,21 +12271,15 @@ namespace ts {
1227712271
isSimpleTypeRelatedTo(source, target, relation, reportErrors ? reportError : undefined)) return Ternary.True;
1227812272

1227912273
const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes);
12280-
if (isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral) {
12274+
const isPerformingExcessPropertyChecks = (isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral);
12275+
if (isPerformingExcessPropertyChecks) {
1228112276
const discriminantType = target.flags & TypeFlags.Union ? findMatchingDiscriminantType(source, target as UnionType) : undefined;
1228212277
if (hasExcessProperties(<FreshObjectLiteralType>source, target, discriminantType, reportErrors)) {
1228312278
if (reportErrors) {
1228412279
reportRelationError(headMessage, source, target);
1228512280
}
1228612281
return Ternary.False;
1228712282
}
12288-
// Above we check for excess properties with respect to the entire target type. When union
12289-
// and intersection types are further deconstructed on the target side, we don't want to
12290-
// make the check again (as it might fail for a partial target type). Therefore we obtain
12291-
// the regular source type and proceed with that.
12292-
if (isUnionOrIntersectionTypeWithoutNullableConstituents(target) && !discriminantType) {
12293-
source = getRegularTypeOfObjectLiteral(source);
12294-
}
1229512283
}
1229612284

1229712285
if (relation !== comparableRelation && !isApparentIntersectionConstituent &&
@@ -12327,11 +12315,24 @@ namespace ts {
1232712315
}
1232812316
else {
1232912317
if (target.flags & TypeFlags.Union) {
12330-
result = typeRelatedToSomeType(source, <UnionType>target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive));
12318+
result = typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), <UnionType>target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive));
12319+
if (result && isPerformingExcessPropertyChecks) {
12320+
// Validate against excess props using the original `source`
12321+
const discriminantType = target.flags & TypeFlags.Union ? findMatchingDiscriminantType(source, target as UnionType) : undefined;
12322+
if (!propertiesRelatedTo(source, discriminantType || target, reportErrors)) {
12323+
return Ternary.False;
12324+
}
12325+
}
1233112326
}
1233212327
else if (target.flags & TypeFlags.Intersection) {
1233312328
isIntersectionConstituent = true; // set here to affect the following trio of checks
12334-
result = typeRelatedToEachType(source, target as IntersectionType, reportErrors);
12329+
result = typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as IntersectionType, reportErrors);
12330+
if (result && isPerformingExcessPropertyChecks) {
12331+
// Validate against excess props using the original `source`
12332+
if (!propertiesRelatedTo(source, target, reportErrors)) {
12333+
return Ternary.False;
12334+
}
12335+
}
1233512336
}
1233612337
else if (source.flags & TypeFlags.Intersection) {
1233712338
// Check to see if any constituents of the intersection are immediately related to the target.
@@ -12431,7 +12432,7 @@ namespace ts {
1243112432
// check excess properties against discriminant type only, not the entire union
1243212433
return hasExcessProperties(source, discriminant, /*discriminant*/ undefined, reportErrors);
1243312434
}
12434-
for (const prop of getPropertiesOfObjectType(source)) {
12435+
for (const prop of getPropertiesOfType(source)) {
1243512436
if (shouldCheckAsExcessProperty(prop, source.symbol) && !isKnownProperty(target, prop.escapedName, isComparingJsxAttributes)) {
1243612437
if (reportErrors) {
1243712438
// Report error in terms of object types in the target as those are the only ones
@@ -13150,7 +13151,9 @@ namespace ts {
1315013151
}
1315113152
}
1315213153
}
13153-
const properties = getPropertiesOfObjectType(target);
13154+
// 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_
13155+
// from the target union, across all members
13156+
const properties = target.flags & TypeFlags.Union ? getPossiblePropertiesOfUnionType(target as UnionType) : getPropertiesOfType(target);
1315413157
for (const targetProp of properties) {
1315513158
if (!(targetProp.flags & SymbolFlags.Prototype)) {
1315613159
const sourceProp = getPropertyOfType(source, targetProp.escapedName);
@@ -14550,9 +14553,9 @@ namespace ts {
1455014553
}
1455114554

1455214555
function* getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean) {
14553-
const properties = target.flags & TypeFlags.Intersection ? getPropertiesOfUnionOrIntersectionType(<IntersectionType>target) : getPropertiesOfObjectType(target);
14556+
const properties = target.flags & TypeFlags.Union ? getPossiblePropertiesOfUnionType(target as UnionType) : getPropertiesOfType(target);
1455414557
for (const targetProp of properties) {
14555-
if (requireOptionalProperties || !(targetProp.flags & SymbolFlags.Optional)) {
14558+
if (requireOptionalProperties || !(targetProp.flags & SymbolFlags.Optional || getCheckFlags(targetProp) & CheckFlags.Partial)) {
1455614559
const sourceProp = getPropertyOfType(source, targetProp.escapedName);
1455714560
if (!sourceProp) {
1455814561
yield targetProp;

src/compiler/types.ts

Lines changed: 14 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 */
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+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//// [deepExcessPropertyCheckingWhenTargetIsIntersection.ts]
2+
interface StatelessComponent<P = {}> {
3+
(props: P & { children?: number }, context?: any): null;
4+
}
5+
6+
const TestComponent: StatelessComponent<TestProps> = (props) => {
7+
return null;
8+
}
9+
10+
interface ITestProps {
11+
ariaLabel?: string;
12+
}
13+
14+
interface NestedProp<TProps> {
15+
props: TProps;
16+
}
17+
18+
interface TestProps {
19+
icon: NestedProp<ITestProps>;
20+
}
21+
22+
TestComponent({icon: { props: { INVALID_PROP_NAME: 'share', ariaLabel: 'test label' } }});
23+
24+
const TestComponent2: StatelessComponent<TestProps | {props2: {x: number}}> = (props) => {
25+
return null;
26+
}
27+
28+
TestComponent2({icon: { props: { INVALID_PROP_NAME: 'share', ariaLabel: 'test label' } }});
29+
30+
31+
//// [deepExcessPropertyCheckingWhenTargetIsIntersection.js]
32+
var TestComponent = function (props) {
33+
return null;
34+
};
35+
TestComponent({ icon: { props: { INVALID_PROP_NAME: 'share', ariaLabel: 'test label' } } });
36+
var TestComponent2 = function (props) {
37+
return null;
38+
};
39+
TestComponent2({ icon: { props: { INVALID_PROP_NAME: 'share', ariaLabel: 'test label' } } });

0 commit comments

Comments
 (0)