Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Infer contextual types from generic return types #29478

Merged
merged 15 commits into from
Feb 1, 2019
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
69 changes: 52 additions & 17 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14632,18 +14632,18 @@ namespace ts {
}

function inferFromProperties(source: Type, target: Type) {
if (isTupleType(source)) {
if (isArrayType(source) || isTupleType(source)) {
if (isTupleType(target)) {
const sourceLength = getLengthOfTupleType(source);
const sourceLength = isTupleType(source) ? getLengthOfTupleType(source) : 0;
const targetLength = getLengthOfTupleType(target);
const sourceRestType = getRestTypeOfTupleType(source);
const sourceRestType = isTupleType(source) ? getRestTypeOfTupleType(source) : getElementTypeOfArrayType(source);
const targetRestType = getRestTypeOfTupleType(target);
const fixedLength = targetLength < sourceLength || sourceRestType ? targetLength : sourceLength;
for (let i = 0; i < fixedLength; i++) {
inferFromTypes(i < sourceLength ? source.typeArguments![i] : sourceRestType!, target.typeArguments![i]);
inferFromTypes(i < sourceLength ? (<TypeReference>source).typeArguments![i] : sourceRestType!, target.typeArguments![i]);
}
if (targetRestType) {
const types = fixedLength < sourceLength ? source.typeArguments!.slice(fixedLength, sourceLength) : [];
const types = fixedLength < sourceLength ? (<TypeReference>source).typeArguments!.slice(fixedLength, sourceLength) : [];
if (sourceRestType) {
types.push(sourceRestType);
}
Expand Down Expand Up @@ -17753,19 +17753,49 @@ namespace ts {
// Return the contextual type for a given expression node. During overload resolution, a contextual type may temporarily
// be "pushed" onto a node using the contextualType property.
function getApparentTypeOfContextualType(node: Expression): Type | undefined {
let contextualType = getContextualType(node);
contextualType = contextualType && mapType(contextualType, getApparentType);
if (contextualType && contextualType.flags & TypeFlags.Union) {
if (isObjectLiteralExpression(node)) {
return discriminateContextualTypeByObjectMembers(node, contextualType as UnionType);
const contextualType = instantiateContextualType(getContextualType(node), node);
if (contextualType) {
const apparentType = mapType(contextualType, getApparentType, /*noReductions*/ true);
if (apparentType.flags & TypeFlags.Union) {
if (isObjectLiteralExpression(node)) {
return discriminateContextualTypeByObjectMembers(node, apparentType as UnionType);
}
else if (isJsxAttributes(node)) {
return discriminateContextualTypeByJSXAttributes(node, apparentType as UnionType);
}
}
else if (isJsxAttributes(node)) {
return discriminateContextualTypeByJSXAttributes(node, contextualType as UnionType);
return apparentType;
}
}

// If the given contextual type contains instantiable types and if a mapper representing
// return type inferences is available, instantiate those types using that mapper.
function instantiateContextualType(contextualType: Type | undefined, node: Expression): Type | undefined {
if (contextualType && maybeTypeOfKind(contextualType, TypeFlags.Instantiable)) {
const returnMapper = (<InferenceContext>getContextualMapper(node)).returnMapper;
if (returnMapper) {
return instantiateInstantiableTypes(contextualType, returnMapper);
}
}
return contextualType;
}

// This function is similar to instantiateType, except that (a) it only instantiates types that
// are classified as instantiable (i.e. it doesn't instantiate object types), and (b) it performs
// no reductions on instantiated union types.
function instantiateInstantiableTypes(type: Type, mapper: TypeMapper): Type {
if (type.flags & TypeFlags.Instantiable) {
return instantiateType(type, mapper);
}
if (type.flags & TypeFlags.Union) {
return getUnionType(map((<UnionType>type).types, t => instantiateInstantiableTypes(t, mapper)), UnionReduction.None);
}
if (type.flags & TypeFlags.Intersection) {
return getIntersectionType(map((<IntersectionType>type).types, t => instantiateInstantiableTypes(t, mapper)));
}
return type;
}

/**
* Woah! Do you really want to use this function?
*
Expand Down Expand Up @@ -19904,6 +19934,9 @@ namespace ts {
const inferenceTargetType = getReturnTypeOfSignature(signature);
// Inferences made from return types have lower priority than all other inferences.
inferTypes(context.inferences, inferenceSourceType, inferenceTargetType, InferencePriority.ReturnType);
// Create a type mapper for instantiating generic contextual types using the inferences made
// from the return type.
context.returnMapper = cloneTypeMapper(context);
}
}

Expand Down Expand Up @@ -23014,7 +23047,12 @@ namespace ts {
context.contextualMapper = contextualMapper;
const checkMode = contextualMapper === identityMapper ? CheckMode.SkipContextSensitive :
contextualMapper ? CheckMode.Inferential : CheckMode.Contextual;
const result = checkExpression(node, checkMode);
const type = checkExpression(node, checkMode);
// We strip literal freshness when an appropriate contextual type is present such that contextually typed
// literals always preserve their literal types (otherwise they might widen during type inference). An alternative
// here would be to not mark contextually typed literals as fresh in the first place.
const result = maybeTypeOfKind(type, TypeFlags.Literal) && isLiteralOfContextualType(type, instantiateContextualType(contextualType, node)) ?
getRegularTypeOfLiteralType(type) : type;
context.contextualType = saveContextualType;
context.contextualMapper = saveContextualMapper;
return result;
Expand Down Expand Up @@ -23098,13 +23136,10 @@ namespace ts {
}

function checkExpressionForMutableLocation(node: Expression, checkMode: CheckMode | undefined, contextualType?: Type, forceTuple?: boolean): Type {
if (arguments.length === 2) {
contextualType = getContextualType(node);
}
const type = checkExpression(node, checkMode, forceTuple);
return isConstContext(node) ? getRegularTypeOfLiteralType(type) :
isTypeAssertion(node) ? type :
getWidenedLiteralLikeTypeForContextualType(type, contextualType);
getWidenedLiteralLikeTypeForContextualType(type, instantiateContextualType(arguments.length === 2 ? getContextualType(node) : contextualType, node));
}

function checkPropertyAssignment(node: PropertyAssignment, checkMode?: CheckMode): Type {
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4369,6 +4369,7 @@ namespace ts {
inferences: InferenceInfo[]; // Inferences made for each type parameter
flags: InferenceFlags; // Inference flags
compareTypes: TypeComparer; // Type comparer function
returnMapper?: TypeMapper; // Type mapper for inferences from return types (if any)
}

/* @internal */
Expand Down
39 changes: 38 additions & 1 deletion tests/baselines/reference/contextualTypeShouldBeLiteral.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,34 @@ let xyz: LikeA | LikeB = {
}
};

xyz;
xyz;

// Repro from #29168

interface TestObject {
type?: 'object';
items: {
[k: string]: TestGeneric;
};
}

interface TestString {
type: 'string';
}

type TestGeneric = (TestString | TestObject) & { [k: string]: any; };

const test: TestGeneric = {
items: {
hello: { type: 'string' },
world: {
items: {
nested: { type: 'string' }
}
}
}
};


//// [contextualTypeShouldBeLiteral.js]
"use strict";
Expand Down Expand Up @@ -134,3 +161,13 @@ var xyz = {
}
};
xyz;
var test = {
items: {
hello: { type: 'string' },
world: {
items: {
nested: { type: 'string' }
}
}
}
};
56 changes: 56 additions & 0 deletions tests/baselines/reference/contextualTypeShouldBeLiteral.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,59 @@ let xyz: LikeA | LikeB = {
xyz;
>xyz : Symbol(xyz, Decl(contextualTypeShouldBeLiteral.ts, 82, 3))

// Repro from #29168

interface TestObject {
>TestObject : Symbol(TestObject, Decl(contextualTypeShouldBeLiteral.ts, 94, 4))

type?: 'object';
>type : Symbol(TestObject.type, Decl(contextualTypeShouldBeLiteral.ts, 98, 22))

items: {
>items : Symbol(TestObject.items, Decl(contextualTypeShouldBeLiteral.ts, 99, 18))

[k: string]: TestGeneric;
>k : Symbol(k, Decl(contextualTypeShouldBeLiteral.ts, 101, 5))
>TestGeneric : Symbol(TestGeneric, Decl(contextualTypeShouldBeLiteral.ts, 107, 1))

};
}

interface TestString {
>TestString : Symbol(TestString, Decl(contextualTypeShouldBeLiteral.ts, 103, 1))

type: 'string';
>type : Symbol(TestString.type, Decl(contextualTypeShouldBeLiteral.ts, 105, 22))
}

type TestGeneric = (TestString | TestObject) & { [k: string]: any; };
>TestGeneric : Symbol(TestGeneric, Decl(contextualTypeShouldBeLiteral.ts, 107, 1))
>TestString : Symbol(TestString, Decl(contextualTypeShouldBeLiteral.ts, 103, 1))
>TestObject : Symbol(TestObject, Decl(contextualTypeShouldBeLiteral.ts, 94, 4))
>k : Symbol(k, Decl(contextualTypeShouldBeLiteral.ts, 109, 50))

const test: TestGeneric = {
>test : Symbol(test, Decl(contextualTypeShouldBeLiteral.ts, 111, 5))
>TestGeneric : Symbol(TestGeneric, Decl(contextualTypeShouldBeLiteral.ts, 107, 1))

items: {
>items : Symbol(items, Decl(contextualTypeShouldBeLiteral.ts, 111, 27))

hello: { type: 'string' },
>hello : Symbol(hello, Decl(contextualTypeShouldBeLiteral.ts, 112, 10))
>type : Symbol(type, Decl(contextualTypeShouldBeLiteral.ts, 113, 12))

world: {
>world : Symbol(world, Decl(contextualTypeShouldBeLiteral.ts, 113, 30))

items: {
>items : Symbol(items, Decl(contextualTypeShouldBeLiteral.ts, 114, 12))

nested: { type: 'string' }
>nested : Symbol(nested, Decl(contextualTypeShouldBeLiteral.ts, 115, 14))
>type : Symbol(type, Decl(contextualTypeShouldBeLiteral.ts, 116, 17))
}
}
}
};

56 changes: 56 additions & 0 deletions tests/baselines/reference/contextualTypeShouldBeLiteral.types
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,59 @@ let xyz: LikeA | LikeB = {
xyz;
>xyz : LikeA

// Repro from #29168

interface TestObject {
type?: 'object';
>type : "object" | undefined

items: {
>items : { [k: string]: TestGeneric; }

[k: string]: TestGeneric;
>k : string

};
}

interface TestString {
type: 'string';
>type : "string"
}

type TestGeneric = (TestString | TestObject) & { [k: string]: any; };
>TestGeneric : TestGeneric
>k : string

const test: TestGeneric = {
>test : TestGeneric
>{ items: { hello: { type: 'string' }, world: { items: { nested: { type: 'string' } } } }} : { items: { hello: { type: "string"; }; world: { items: { nested: { type: "string"; }; }; }; }; }

items: {
>items : { hello: { type: "string"; }; world: { items: { nested: { type: "string"; }; }; }; }
>{ hello: { type: 'string' }, world: { items: { nested: { type: 'string' } } } } : { hello: { type: "string"; }; world: { items: { nested: { type: "string"; }; }; }; }

hello: { type: 'string' },
>hello : { type: "string"; }
>{ type: 'string' } : { type: "string"; }
>type : "string"
>'string' : "string"

world: {
>world : { items: { nested: { type: "string"; }; }; }
>{ items: { nested: { type: 'string' } } } : { items: { nested: { type: "string"; }; }; }

items: {
>items : { nested: { type: "string"; }; }
>{ nested: { type: 'string' } } : { nested: { type: "string"; }; }

nested: { type: 'string' }
>nested : { type: "string"; }
>{ type: 'string' } : { type: "string"; }
>type : "string"
>'string' : "string"
}
}
}
};

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
tests/cases/compiler/errorMessagesIntersectionTypes02.ts(14,5): error TS2322: Type '{ fooProp: string; } & Bar' is not assignable to type 'FooBar'.
tests/cases/compiler/errorMessagesIntersectionTypes02.ts(14,5): error TS2322: Type '{ fooProp: "frizzlebizzle"; } & Bar' is not assignable to type 'FooBar'.
Types of property 'fooProp' are incompatible.
Type 'string' is not assignable to type '"hello" | "world"'.
Type '"frizzlebizzle"' is not assignable to type '"hello" | "world"'.


==== tests/cases/compiler/errorMessagesIntersectionTypes02.ts (1 errors) ====
Expand All @@ -19,8 +19,8 @@ tests/cases/compiler/errorMessagesIntersectionTypes02.ts(14,5): error TS2322: Ty

let fooBar: FooBar = mixBar({
~~~~~~
!!! error TS2322: Type '{ fooProp: string; } & Bar' is not assignable to type 'FooBar'.
!!! error TS2322: Type '{ fooProp: "frizzlebizzle"; } & Bar' is not assignable to type 'FooBar'.
!!! error TS2322: Types of property 'fooProp' are incompatible.
!!! error TS2322: Type 'string' is not assignable to type '"hello" | "world"'.
!!! error TS2322: Type '"frizzlebizzle"' is not assignable to type '"hello" | "world"'.
fooProp: "frizzlebizzle"
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ declare function mixBar<T>(obj: T): T & Bar;

let fooBar: FooBar = mixBar({
>fooBar : FooBar
>mixBar({ fooProp: "frizzlebizzle"}) : { fooProp: string; } & Bar
>mixBar({ fooProp: "frizzlebizzle"}) : { fooProp: "frizzlebizzle"; } & Bar
>mixBar : <T>(obj: T) => T & Bar
>{ fooProp: "frizzlebizzle"} : { fooProp: string; }
>{ fooProp: "frizzlebizzle"} : { fooProp: "frizzlebizzle"; }

fooProp: "frizzlebizzle"
>fooProp : string
>fooProp : "frizzlebizzle"
>"frizzlebizzle" : "frizzlebizzle"

});
Loading