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

Improved type inference for object literals #19513

Merged
merged 18 commits into from
Oct 29, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 130 additions & 31 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ namespace ts {
const literalTypes = createMap<LiteralType>();
const indexedAccessTypes = createMap<IndexedAccessType>();
const evolvingArrayTypes: EvolvingArrayType[] = [];
const undefinedProperties = createMap<Symbol>() as UnderscoreEscapedMap<Symbol>;

const unknownSymbol = createSymbol(SymbolFlags.Property, "unknown" as __String);
const resolvingSymbol = createSymbol(0, InternalSymbolName.Resolving);
Expand Down Expand Up @@ -7984,7 +7985,7 @@ namespace ts {
const spread = createAnonymousType(undefined, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo);
spread.flags |= propagatedFlags;
spread.flags |= TypeFlags.FreshLiteral | TypeFlags.ContainsObjectLiteral;
(spread as ObjectType).objectFlags |= ObjectFlags.ObjectLiteral;
(spread as ObjectType).objectFlags |= (ObjectFlags.ObjectLiteral | ObjectFlags.ContainsSpread);
spread.symbol = symbol;
return spread;
}
Expand Down Expand Up @@ -9026,7 +9027,7 @@ namespace ts {

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

if (getObjectFlags(source) & ObjectFlags.ObjectLiteral && source.flags & TypeFlags.FreshLiteral) {
if (isObjectLiteralType(source) && source.flags & TypeFlags.FreshLiteral) {
if (hasExcessProperties(<FreshObjectLiteralType>source, target, reportErrors)) {
if (reportErrors) {
reportRelationError(headMessage, source, target);
Expand Down Expand Up @@ -9596,14 +9597,27 @@ namespace ts {
if (relation === identityRelation) {
return propertiesIdenticalTo(source, target);
}
const requireOptionalProperties = relation === subtypeRelation && !(getObjectFlags(source) & ObjectFlags.ObjectLiteral);
const requireOptionalProperties = relation === subtypeRelation && !isObjectLiteralType(source);
const unmatchedProperty = getUnmatchedProperty(source, target, requireOptionalProperties);
if (unmatchedProperty) {
if (reportErrors) {
reportError(Diagnostics.Property_0_is_missing_in_type_1, symbolToString(unmatchedProperty), typeToString(source));
}
return Ternary.False;
}
if (isObjectLiteralType(target)) {
for (const sourceProp of getPropertiesOfType(source)) {
if (!getPropertyOfObjectType(target, sourceProp.escapedName)) {
const sourceType = getTypeOfSymbol(sourceProp);
if (!(sourceType === undefinedType || sourceType === undefinedWideningType)) {
if (reportErrors) {
reportError(Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(sourceProp), typeToString(target));
}
return Ternary.False;
}
}
}
}
let result = Ternary.True;
const properties = getPropertiesOfObjectType(target);
for (const targetProp of properties) {
Expand Down Expand Up @@ -10425,7 +10439,7 @@ namespace ts {
* Leave signatures alone since they are not subject to the check.
*/
function getRegularTypeOfObjectLiteral(type: Type): Type {
if (!(getObjectFlags(type) & ObjectFlags.ObjectLiteral && type.flags & TypeFlags.FreshLiteral)) {
if (!(isObjectLiteralType(type) && type.flags & TypeFlags.FreshLiteral)) {
return type;
}
const regularType = (<FreshObjectLiteralType>type).regularType;
Expand All @@ -10447,18 +10461,74 @@ namespace ts {
return regularNew;
}

function getWidenedProperty(prop: Symbol): Symbol {
function createWideningContext(parent: WideningContext, propertyName: __String, siblings: Type[]): WideningContext {
return { parent, propertyName, siblings, resolvedPropertyNames: undefined };
}

function getSiblingsOfContext(context: WideningContext): Type[] {
if (!context.siblings) {
const siblings: Type[] = [];
for (const type of getSiblingsOfContext(context.parent)) {
if (isObjectLiteralType(type)) {
const prop = getPropertyOfObjectType(type, context.propertyName);
if (prop) {
forEachType(getTypeOfSymbol(prop), t => {
siblings.push(t);
});
}
}
}
context.siblings = siblings;
}
return context.siblings;
}

function getPropertyNamesOfContext(context: WideningContext): __String[] {
if (!context.resolvedPropertyNames) {
const names = createMap<boolean>() as UnderscoreEscapedMap<boolean>;
for (const t of getSiblingsOfContext(context)) {
if (isObjectLiteralType(t) && !(getObjectFlags(t) & ObjectFlags.ContainsSpread)) {
for (const prop of getPropertiesOfType(t)) {
names.set(prop.escapedName, true);
}
}
}
context.resolvedPropertyNames = arrayFrom(names.keys());
}
return context.resolvedPropertyNames;
}

function getWidenedProperty(prop: Symbol, context: WideningContext): Symbol {
const original = getTypeOfSymbol(prop);
const widened = getWidenedType(original);
const propContext = context && createWideningContext(context, prop.escapedName, /*siblings*/ undefined);
const widened = getWidenedTypeWithContext(original, propContext);
return widened === original ? prop : createSymbolWithType(prop, widened);
}

function getWidenedTypeOfObjectLiteral(type: Type): Type {
function getUndefinedProperty(name: __String) {
const cached = undefinedProperties.get(name);
if (cached) {
return cached;
}
const result = createSymbol(SymbolFlags.Property | SymbolFlags.Optional, name);
result.type = undefinedType;
undefinedProperties.set(name, result);
return result;
}

function getWidenedTypeOfObjectLiteral(type: Type, context: WideningContext): Type {
const members = createSymbolTable();
for (const prop of getPropertiesOfObjectType(type)) {
// Since get accessors already widen their return value there is no need to
// widen accessor based properties here.
members.set(prop.escapedName, prop.flags & SymbolFlags.Property ? getWidenedProperty(prop) : prop);
members.set(prop.escapedName, prop.flags & SymbolFlags.Property ? getWidenedProperty(prop, context) : prop);
}
if (context) {
for (const name of getPropertyNamesOfContext(context)) {
if (!members.has(name)) {
members.set(name, getUndefinedProperty(name));
}
}
}
const stringIndexInfo = getIndexInfoOfType(type, IndexKind.String);
const numberIndexInfo = getIndexInfoOfType(type, IndexKind.Number);
Expand All @@ -10467,20 +10537,25 @@ namespace ts {
numberIndexInfo && createIndexInfo(getWidenedType(numberIndexInfo.type), numberIndexInfo.isReadonly));
}

function getWidenedConstituentType(type: Type): Type {
return type.flags & TypeFlags.Nullable ? type : getWidenedType(type);
function getWidenedType(type: Type) {
return getWidenedTypeWithContext(type, /*context*/ undefined);
}

function getWidenedType(type: Type): Type {
function getWidenedTypeWithContext(type: Type, context: WideningContext): Type {
if (type.flags & TypeFlags.RequiresWidening) {
if (type.flags & TypeFlags.Nullable) {
return anyType;
}
if (getObjectFlags(type) & ObjectFlags.ObjectLiteral) {
return getWidenedTypeOfObjectLiteral(type);
if (isObjectLiteralType(type)) {
return getWidenedTypeOfObjectLiteral(type, context);
}
if (type.flags & TypeFlags.Union) {
return getUnionType(sameMap((<UnionType>type).types, getWidenedConstituentType));
const unionContext = context || createWideningContext(/*parent*/ undefined, /*propertyName*/ undefined, (<UnionType>type).types);
const widenedTypes = sameMap((<UnionType>type).types, t => t.flags & TypeFlags.Nullable ? t : getWidenedTypeWithContext(t, unionContext));
// Widening an empty object literal transitions from a highly restrictive type to
// a highly inclusive one. For that reason we perform subtype reduction here if the
// union includes empty object types (e.g. reducing {} | string to just {}).
return getUnionType(widenedTypes, some(widenedTypes, isEmptyObjectType));
}
if (isArrayType(type) || isTupleType(type)) {
return createTypeReference((<TypeReference>type).target, sameMap((<TypeReference>type).typeArguments, getWidenedType));
Expand All @@ -10502,28 +10577,35 @@ namespace ts {
*/
function reportWideningErrorsInType(type: Type): boolean {
let errorReported = false;
if (type.flags & TypeFlags.Union) {
for (const t of (<UnionType>type).types) {
if (reportWideningErrorsInType(t)) {
if (type.flags & TypeFlags.ContainsWideningType) {
if (type.flags & TypeFlags.Union) {
if (some((<UnionType>type).types, isEmptyObjectType)) {
errorReported = true;
}
else {
for (const t of (<UnionType>type).types) {
if (reportWideningErrorsInType(t)) {
errorReported = true;
}
}
}
}
}
if (isArrayType(type) || isTupleType(type)) {
for (const t of (<TypeReference>type).typeArguments) {
if (reportWideningErrorsInType(t)) {
errorReported = true;
if (isArrayType(type) || isTupleType(type)) {
for (const t of (<TypeReference>type).typeArguments) {
if (reportWideningErrorsInType(t)) {
errorReported = true;
}
}
}
}
if (getObjectFlags(type) & ObjectFlags.ObjectLiteral) {
for (const p of getPropertiesOfObjectType(type)) {
const t = getTypeOfSymbol(p);
if (t.flags & TypeFlags.ContainsWideningType) {
if (!reportWideningErrorsInType(t)) {
error(p.valueDeclaration, Diagnostics.Object_literal_s_property_0_implicitly_has_an_1_type, symbolName(p), typeToString(getWidenedType(t)));
if (isObjectLiteralType(type)) {
for (const p of getPropertiesOfObjectType(type)) {
const t = getTypeOfSymbol(p);
if (t.flags & TypeFlags.ContainsWideningType) {
if (!reportWideningErrorsInType(t)) {
error(p.valueDeclaration, Diagnostics.Object_literal_s_property_0_implicitly_has_an_1_type, symbolName(p), typeToString(getWidenedType(t)));
}
errorReported = true;
}
errorReported = true;
}
}
}
Expand Down Expand Up @@ -11029,11 +11111,28 @@ namespace ts {
return constraint && maybeTypeOfKind(constraint, TypeFlags.Primitive | TypeFlags.Index);
}

function isObjectLiteralType(type: Type) {
return !!(getObjectFlags(type) & ObjectFlags.ObjectLiteral);
}

function widenObjectLiteralCandidates(candidates: Type[]): Type[] {
if (candidates.length > 1) {
const objectLiterals = filter(candidates, isObjectLiteralType);
if (objectLiterals.length) {
const objectLiteralsType = getWidenedType(getUnionType(objectLiterals, /*subtypeReduction*/ true));
return concatenate(filter(candidates, t => !isObjectLiteralType(t)), [objectLiteralsType]);
}
}
return candidates;
}

function getInferredType(context: InferenceContext, index: number): Type {
const inference = context.inferences[index];
let inferredType = inference.inferredType;
if (!inferredType) {
if (inference.candidates) {
// Extract all object literal types and replace them with a single widened and normalized type.
const candidates = widenObjectLiteralCandidates(inference.candidates);
// We widen inferred literal types if
// all inferences were made to top-level ocurrences of the type parameter, and
// the type parameter has no constraint or its constraint includes no primitive or literal types, and
Expand All @@ -11042,7 +11141,7 @@ namespace ts {
const widenLiteralTypes = inference.topLevel &&
!hasPrimitiveConstraint(inference.typeParameter) &&
(inference.isFixed || !isTypeParameterAtTopLevel(getReturnTypeOfSignature(signature), inference.typeParameter));
const baseCandidates = widenLiteralTypes ? sameMap(inference.candidates, getWidenedLiteralType) : inference.candidates;
const baseCandidates = widenLiteralTypes ? sameMap(candidates, getWidenedLiteralType) : candidates;
// If all inferences were made from contravariant positions, infer a common subtype. Otherwise, if
// union types were requested or if all inferences were made from the return type position, infer a
// union type. Otherwise, infer a common supertype.
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3327,6 +3327,7 @@ namespace ts {
ObjectLiteral = 1 << 7, // Originates in an object literal
EvolvingArray = 1 << 8, // Evolving array type
ObjectLiteralPatternWithComputedProperties = 1 << 9, // Object literal pattern with computed properties
ContainsSpread = 1 << 10, // Object literal contains spread operation
ClassOrInterface = Class | Interface
}

Expand Down Expand Up @@ -3609,6 +3610,14 @@ namespace ts {
compareTypes: TypeComparer; // Type comparer function
}

/* @internal */
export interface WideningContext {
parent?: WideningContext; // Parent context
propertyName?: __String; // Name of property in parent
siblings?: Type[]; // Types of siblings
resolvedPropertyNames?: __String[]; // Property names occurring in sibling object literals
}

/* @internal */
export const enum SpecialPropertyAssignmentKind {
None,
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2040,6 +2040,7 @@ declare namespace ts {
ObjectLiteral = 128,
EvolvingArray = 256,
ObjectLiteralPatternWithComputedProperties = 512,
ContainsSpread = 1024,
ClassOrInterface = 3,
}
interface ObjectType extends Type {
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2040,6 +2040,7 @@ declare namespace ts {
ObjectLiteral = 128,
EvolvingArray = 256,
ObjectLiteralPatternWithComputedProperties = 512,
ContainsSpread = 1024,
ClassOrInterface = 3,
}
interface ObjectType extends Type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclarati
tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts(1,33): error TS2304: Cannot find name 'await'.
tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts(1,38): error TS1005: ';' expected.
tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts(1,39): error TS1128: Declaration or statement expected.
tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts(1,41): error TS2365: Operator '>' cannot be applied to types 'boolean' and '{}'.
tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts(1,49): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts(1,53): error TS1109: Expression expected.


==== tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts (8 errors) ====
==== tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclaration10_es2017.ts (9 errors) ====
async function foo(a = await => await): Promise<void> {
~~~~~~~~~
!!! error TS2371: A parameter initializer is only allowed in a function or constructor implementation.
Expand All @@ -22,8 +23,11 @@ tests/cases/conformance/async/es2017/functionDeclarations/asyncFunctionDeclarati
!!! error TS1005: ';' expected.
~
!!! error TS1128: Declaration or statement expected.
~~~~~~~~~~~~~~~
~~~~
!!! error TS2532: Object is possibly 'undefined'.
~
!!! error TS1109: Expression expected.
}
}
~
!!! error TS2365: Operator '>' cannot be applied to types 'boolean' and '{}'.
Loading