Merge pull request #19513 from Microsoft/strictObjectLiterals

Improved type inference for object literals
This commit is contained in:
Anders Hejlsberg
2017-10-28 17:13:21 -07:00
committed by GitHub
40 changed files with 1199 additions and 141 deletions

View File

@@ -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);
@@ -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;
}
@@ -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);
@@ -9596,7 +9597,7 @@ 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) {
@@ -9604,6 +9605,19 @@ namespace ts {
}
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) {
@@ -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;
@@ -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);
@@ -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));
@@ -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;
}
}
}
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)));
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 (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;
}
}
}
@@ -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
@@ -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.

View File

@@ -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
}
@@ -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,