Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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 @@ -14608,18 +14608,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 @@ -17714,19 +17714,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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mapType call should probably have it's third argument set since we're in contextual typing and preserving string | "x"'s literaliness is desirable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: I'm wondering why bother with the map type/instantiate flag check at all - instantiateType is only going to instantiate instantiable types anyway.

Copy link
Copy Markdown
Collaborator

@jack-williams jack-williams Jan 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you change the mapType I think you can go ahead and close #29174.

(or ping me and I’ll close it)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weswigham Agreed, we should make sure we use UnionReduction.None. The reason we don't just call instantiateType is that getContextualMapper isn't a super cheap operation and also that we don't want to instantiate contained object types as it is unnecessary and can be expensive.

@jack-williams I'll fix it so we don't need #29174.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahejlsberg Grand, closing the PR. Might be worth adding the example from #29168 to this PR, just to make sure it’s fixed.

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 @@ -19866,6 +19896,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 @@ -22949,7 +22982,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 @@ -23026,12 +23064,9 @@ 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 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 @@ -4368,6 +4368,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