Skip to content

Commit 7798f69

Browse files
authored
Merge pull request #19513 from Microsoft/strictObjectLiterals
Improved type inference for object literals
2 parents b5f292d + fd0d40c commit 7798f69

40 files changed

+1196
-138
lines changed

src/compiler/checker.ts

+130-31
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ namespace ts {
260260
const literalTypes = createMap<LiteralType>();
261261
const indexedAccessTypes = createMap<IndexedAccessType>();
262262
const evolvingArrayTypes: EvolvingArrayType[] = [];
263+
const undefinedProperties = createMap<Symbol>() as UnderscoreEscapedMap<Symbol>;
263264

264265
const unknownSymbol = createSymbol(SymbolFlags.Property, "unknown" as __String);
265266
const resolvingSymbol = createSymbol(0, InternalSymbolName.Resolving);
@@ -7984,7 +7985,7 @@ namespace ts {
79847985
const spread = createAnonymousType(undefined, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo);
79857986
spread.flags |= propagatedFlags;
79867987
spread.flags |= TypeFlags.FreshLiteral | TypeFlags.ContainsObjectLiteral;
7987-
(spread as ObjectType).objectFlags |= ObjectFlags.ObjectLiteral;
7988+
(spread as ObjectType).objectFlags |= (ObjectFlags.ObjectLiteral | ObjectFlags.ContainsSpread);
79887989
spread.symbol = symbol;
79897990
return spread;
79907991
}
@@ -9026,7 +9027,7 @@ namespace ts {
90269027

90279028
if (isSimpleTypeRelatedTo(source, target, relation, reportErrors ? reportError : undefined)) return Ternary.True;
90289029

9029-
if (getObjectFlags(source) & ObjectFlags.ObjectLiteral && source.flags & TypeFlags.FreshLiteral) {
9030+
if (isObjectLiteralType(source) && source.flags & TypeFlags.FreshLiteral) {
90309031
if (hasExcessProperties(<FreshObjectLiteralType>source, target, reportErrors)) {
90319032
if (reportErrors) {
90329033
reportRelationError(headMessage, source, target);
@@ -9596,14 +9597,27 @@ namespace ts {
95969597
if (relation === identityRelation) {
95979598
return propertiesIdenticalTo(source, target);
95989599
}
9599-
const requireOptionalProperties = relation === subtypeRelation && !(getObjectFlags(source) & ObjectFlags.ObjectLiteral);
9600+
const requireOptionalProperties = relation === subtypeRelation && !isObjectLiteralType(source);
96009601
const unmatchedProperty = getUnmatchedProperty(source, target, requireOptionalProperties);
96019602
if (unmatchedProperty) {
96029603
if (reportErrors) {
96039604
reportError(Diagnostics.Property_0_is_missing_in_type_1, symbolToString(unmatchedProperty), typeToString(source));
96049605
}
96059606
return Ternary.False;
96069607
}
9608+
if (isObjectLiteralType(target)) {
9609+
for (const sourceProp of getPropertiesOfType(source)) {
9610+
if (!getPropertyOfObjectType(target, sourceProp.escapedName)) {
9611+
const sourceType = getTypeOfSymbol(sourceProp);
9612+
if (!(sourceType === undefinedType || sourceType === undefinedWideningType)) {
9613+
if (reportErrors) {
9614+
reportError(Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(sourceProp), typeToString(target));
9615+
}
9616+
return Ternary.False;
9617+
}
9618+
}
9619+
}
9620+
}
96079621
let result = Ternary.True;
96089622
const properties = getPropertiesOfObjectType(target);
96099623
for (const targetProp of properties) {
@@ -10425,7 +10439,7 @@ namespace ts {
1042510439
* Leave signatures alone since they are not subject to the check.
1042610440
*/
1042710441
function getRegularTypeOfObjectLiteral(type: Type): Type {
10428-
if (!(getObjectFlags(type) & ObjectFlags.ObjectLiteral && type.flags & TypeFlags.FreshLiteral)) {
10442+
if (!(isObjectLiteralType(type) && type.flags & TypeFlags.FreshLiteral)) {
1042910443
return type;
1043010444
}
1043110445
const regularType = (<FreshObjectLiteralType>type).regularType;
@@ -10447,18 +10461,74 @@ namespace ts {
1044710461
return regularNew;
1044810462
}
1044910463

10450-
function getWidenedProperty(prop: Symbol): Symbol {
10464+
function createWideningContext(parent: WideningContext, propertyName: __String, siblings: Type[]): WideningContext {
10465+
return { parent, propertyName, siblings, resolvedPropertyNames: undefined };
10466+
}
10467+
10468+
function getSiblingsOfContext(context: WideningContext): Type[] {
10469+
if (!context.siblings) {
10470+
const siblings: Type[] = [];
10471+
for (const type of getSiblingsOfContext(context.parent)) {
10472+
if (isObjectLiteralType(type)) {
10473+
const prop = getPropertyOfObjectType(type, context.propertyName);
10474+
if (prop) {
10475+
forEachType(getTypeOfSymbol(prop), t => {
10476+
siblings.push(t);
10477+
});
10478+
}
10479+
}
10480+
}
10481+
context.siblings = siblings;
10482+
}
10483+
return context.siblings;
10484+
}
10485+
10486+
function getPropertyNamesOfContext(context: WideningContext): __String[] {
10487+
if (!context.resolvedPropertyNames) {
10488+
const names = createMap<boolean>() as UnderscoreEscapedMap<boolean>;
10489+
for (const t of getSiblingsOfContext(context)) {
10490+
if (isObjectLiteralType(t) && !(getObjectFlags(t) & ObjectFlags.ContainsSpread)) {
10491+
for (const prop of getPropertiesOfType(t)) {
10492+
names.set(prop.escapedName, true);
10493+
}
10494+
}
10495+
}
10496+
context.resolvedPropertyNames = arrayFrom(names.keys());
10497+
}
10498+
return context.resolvedPropertyNames;
10499+
}
10500+
10501+
function getWidenedProperty(prop: Symbol, context: WideningContext): Symbol {
1045110502
const original = getTypeOfSymbol(prop);
10452-
const widened = getWidenedType(original);
10503+
const propContext = context && createWideningContext(context, prop.escapedName, /*siblings*/ undefined);
10504+
const widened = getWidenedTypeWithContext(original, propContext);
1045310505
return widened === original ? prop : createSymbolWithType(prop, widened);
1045410506
}
1045510507

10456-
function getWidenedTypeOfObjectLiteral(type: Type): Type {
10508+
function getUndefinedProperty(name: __String) {
10509+
const cached = undefinedProperties.get(name);
10510+
if (cached) {
10511+
return cached;
10512+
}
10513+
const result = createSymbol(SymbolFlags.Property | SymbolFlags.Optional, name);
10514+
result.type = undefinedType;
10515+
undefinedProperties.set(name, result);
10516+
return result;
10517+
}
10518+
10519+
function getWidenedTypeOfObjectLiteral(type: Type, context: WideningContext): Type {
1045710520
const members = createSymbolTable();
1045810521
for (const prop of getPropertiesOfObjectType(type)) {
1045910522
// Since get accessors already widen their return value there is no need to
1046010523
// widen accessor based properties here.
10461-
members.set(prop.escapedName, prop.flags & SymbolFlags.Property ? getWidenedProperty(prop) : prop);
10524+
members.set(prop.escapedName, prop.flags & SymbolFlags.Property ? getWidenedProperty(prop, context) : prop);
10525+
}
10526+
if (context) {
10527+
for (const name of getPropertyNamesOfContext(context)) {
10528+
if (!members.has(name)) {
10529+
members.set(name, getUndefinedProperty(name));
10530+
}
10531+
}
1046210532
}
1046310533
const stringIndexInfo = getIndexInfoOfType(type, IndexKind.String);
1046410534
const numberIndexInfo = getIndexInfoOfType(type, IndexKind.Number);
@@ -10467,20 +10537,25 @@ namespace ts {
1046710537
numberIndexInfo && createIndexInfo(getWidenedType(numberIndexInfo.type), numberIndexInfo.isReadonly));
1046810538
}
1046910539

10470-
function getWidenedConstituentType(type: Type): Type {
10471-
return type.flags & TypeFlags.Nullable ? type : getWidenedType(type);
10540+
function getWidenedType(type: Type) {
10541+
return getWidenedTypeWithContext(type, /*context*/ undefined);
1047210542
}
1047310543

10474-
function getWidenedType(type: Type): Type {
10544+
function getWidenedTypeWithContext(type: Type, context: WideningContext): Type {
1047510545
if (type.flags & TypeFlags.RequiresWidening) {
1047610546
if (type.flags & TypeFlags.Nullable) {
1047710547
return anyType;
1047810548
}
10479-
if (getObjectFlags(type) & ObjectFlags.ObjectLiteral) {
10480-
return getWidenedTypeOfObjectLiteral(type);
10549+
if (isObjectLiteralType(type)) {
10550+
return getWidenedTypeOfObjectLiteral(type, context);
1048110551
}
1048210552
if (type.flags & TypeFlags.Union) {
10483-
return getUnionType(sameMap((<UnionType>type).types, getWidenedConstituentType));
10553+
const unionContext = context || createWideningContext(/*parent*/ undefined, /*propertyName*/ undefined, (<UnionType>type).types);
10554+
const widenedTypes = sameMap((<UnionType>type).types, t => t.flags & TypeFlags.Nullable ? t : getWidenedTypeWithContext(t, unionContext));
10555+
// Widening an empty object literal transitions from a highly restrictive type to
10556+
// a highly inclusive one. For that reason we perform subtype reduction here if the
10557+
// union includes empty object types (e.g. reducing {} | string to just {}).
10558+
return getUnionType(widenedTypes, some(widenedTypes, isEmptyObjectType));
1048410559
}
1048510560
if (isArrayType(type) || isTupleType(type)) {
1048610561
return createTypeReference((<TypeReference>type).target, sameMap((<TypeReference>type).typeArguments, getWidenedType));
@@ -10502,28 +10577,35 @@ namespace ts {
1050210577
*/
1050310578
function reportWideningErrorsInType(type: Type): boolean {
1050410579
let errorReported = false;
10505-
if (type.flags & TypeFlags.Union) {
10506-
for (const t of (<UnionType>type).types) {
10507-
if (reportWideningErrorsInType(t)) {
10580+
if (type.flags & TypeFlags.ContainsWideningType) {
10581+
if (type.flags & TypeFlags.Union) {
10582+
if (some((<UnionType>type).types, isEmptyObjectType)) {
1050810583
errorReported = true;
1050910584
}
10585+
else {
10586+
for (const t of (<UnionType>type).types) {
10587+
if (reportWideningErrorsInType(t)) {
10588+
errorReported = true;
10589+
}
10590+
}
10591+
}
1051010592
}
10511-
}
10512-
if (isArrayType(type) || isTupleType(type)) {
10513-
for (const t of (<TypeReference>type).typeArguments) {
10514-
if (reportWideningErrorsInType(t)) {
10515-
errorReported = true;
10593+
if (isArrayType(type) || isTupleType(type)) {
10594+
for (const t of (<TypeReference>type).typeArguments) {
10595+
if (reportWideningErrorsInType(t)) {
10596+
errorReported = true;
10597+
}
1051610598
}
1051710599
}
10518-
}
10519-
if (getObjectFlags(type) & ObjectFlags.ObjectLiteral) {
10520-
for (const p of getPropertiesOfObjectType(type)) {
10521-
const t = getTypeOfSymbol(p);
10522-
if (t.flags & TypeFlags.ContainsWideningType) {
10523-
if (!reportWideningErrorsInType(t)) {
10524-
error(p.valueDeclaration, Diagnostics.Object_literal_s_property_0_implicitly_has_an_1_type, symbolName(p), typeToString(getWidenedType(t)));
10600+
if (isObjectLiteralType(type)) {
10601+
for (const p of getPropertiesOfObjectType(type)) {
10602+
const t = getTypeOfSymbol(p);
10603+
if (t.flags & TypeFlags.ContainsWideningType) {
10604+
if (!reportWideningErrorsInType(t)) {
10605+
error(p.valueDeclaration, Diagnostics.Object_literal_s_property_0_implicitly_has_an_1_type, symbolName(p), typeToString(getWidenedType(t)));
10606+
}
10607+
errorReported = true;
1052510608
}
10526-
errorReported = true;
1052710609
}
1052810610
}
1052910611
}
@@ -11029,11 +11111,28 @@ namespace ts {
1102911111
return constraint && maybeTypeOfKind(constraint, TypeFlags.Primitive | TypeFlags.Index);
1103011112
}
1103111113

11114+
function isObjectLiteralType(type: Type) {
11115+
return !!(getObjectFlags(type) & ObjectFlags.ObjectLiteral);
11116+
}
11117+
11118+
function widenObjectLiteralCandidates(candidates: Type[]): Type[] {
11119+
if (candidates.length > 1) {
11120+
const objectLiterals = filter(candidates, isObjectLiteralType);
11121+
if (objectLiterals.length) {
11122+
const objectLiteralsType = getWidenedType(getUnionType(objectLiterals, /*subtypeReduction*/ true));
11123+
return concatenate(filter(candidates, t => !isObjectLiteralType(t)), [objectLiteralsType]);
11124+
}
11125+
}
11126+
return candidates;
11127+
}
11128+
1103211129
function getInferredType(context: InferenceContext, index: number): Type {
1103311130
const inference = context.inferences[index];
1103411131
let inferredType = inference.inferredType;
1103511132
if (!inferredType) {
1103611133
if (inference.candidates) {
11134+
// Extract all object literal types and replace them with a single widened and normalized type.
11135+
const candidates = widenObjectLiteralCandidates(inference.candidates);
1103711136
// We widen inferred literal types if
1103811137
// all inferences were made to top-level ocurrences of the type parameter, and
1103911138
// the type parameter has no constraint or its constraint includes no primitive or literal types, and
@@ -11042,7 +11141,7 @@ namespace ts {
1104211141
const widenLiteralTypes = inference.topLevel &&
1104311142
!hasPrimitiveConstraint(inference.typeParameter) &&
1104411143
(inference.isFixed || !isTypeParameterAtTopLevel(getReturnTypeOfSignature(signature), inference.typeParameter));
11045-
const baseCandidates = widenLiteralTypes ? sameMap(inference.candidates, getWidenedLiteralType) : inference.candidates;
11144+
const baseCandidates = widenLiteralTypes ? sameMap(candidates, getWidenedLiteralType) : candidates;
1104611145
// If all inferences were made from contravariant positions, infer a common subtype. Otherwise, if
1104711146
// union types were requested or if all inferences were made from the return type position, infer a
1104811147
// union type. Otherwise, infer a common supertype.

src/compiler/types.ts

+9
Original file line numberDiff line numberDiff line change
@@ -3327,6 +3327,7 @@ namespace ts {
33273327
ObjectLiteral = 1 << 7, // Originates in an object literal
33283328
EvolvingArray = 1 << 8, // Evolving array type
33293329
ObjectLiteralPatternWithComputedProperties = 1 << 9, // Object literal pattern with computed properties
3330+
ContainsSpread = 1 << 10, // Object literal contains spread operation
33303331
ClassOrInterface = Class | Interface
33313332
}
33323333

@@ -3609,6 +3610,14 @@ namespace ts {
36093610
compareTypes: TypeComparer; // Type comparer function
36103611
}
36113612

3613+
/* @internal */
3614+
export interface WideningContext {
3615+
parent?: WideningContext; // Parent context
3616+
propertyName?: __String; // Name of property in parent
3617+
siblings?: Type[]; // Types of siblings
3618+
resolvedPropertyNames?: __String[]; // Property names occurring in sibling object literals
3619+
}
3620+
36123621
/* @internal */
36133622
export const enum SpecialPropertyAssignmentKind {
36143623
None,

tests/baselines/reference/api/tsserverlibrary.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2040,6 +2040,7 @@ declare namespace ts {
20402040
ObjectLiteral = 128,
20412041
EvolvingArray = 256,
20422042
ObjectLiteralPatternWithComputedProperties = 512,
2043+
ContainsSpread = 1024,
20432044
ClassOrInterface = 3,
20442045
}
20452046
interface ObjectType extends Type {

tests/baselines/reference/api/typescript.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2040,6 +2040,7 @@ declare namespace ts {
20402040
ObjectLiteral = 128,
20412041
EvolvingArray = 256,
20422042
ObjectLiteralPatternWithComputedProperties = 512,
2043+
ContainsSpread = 1024,
20432044
ClassOrInterface = 3,
20442045
}
20452046
interface ObjectType extends Type {

tests/baselines/reference/asyncFunctionDeclaration10_es2017.errors.txt

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclarati
44
tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts(1,33): error TS2304: Cannot find name 'await'.
55
tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts(1,38): error TS1005: ';' expected.
66
tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts(1,39): error TS1128: Declaration or statement expected.
7+
tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts(1,41): error TS2365: Operator '>' cannot be applied to types 'boolean' and '{}'.
78
tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts(1,49): error TS2532: Object is possibly 'undefined'.
89
tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts(1,53): error TS1109: Expression expected.
910

1011

11-
==== tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts (8 errors) ====
12+
==== tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts (9 errors) ====
1213
async function foo(a = await => await): Promise<void> {
1314
~~~~~~~~~
1415
!!! error TS2371: A parameter initializer is only allowed in a function or constructor implementation.
@@ -22,8 +23,11 @@ tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclarati
2223
!!! error TS1005: ';' expected.
2324
~
2425
!!! error TS1128: Declaration or statement expected.
26+
~~~~~~~~~~~~~~~
2527
~~~~
2628
!!! error TS2532: Object is possibly 'undefined'.
2729
~
2830
!!! error TS1109: Expression expected.
29-
}
31+
}
32+
~
33+
!!! error TS2365: Operator '>' cannot be applied to types 'boolean' and '{}'.

0 commit comments

Comments
 (0)