diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 09cddc53c10e5..8876e9b8862a6 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -7477,7 +7477,7 @@ namespace ts { // expression constructs such as array literals and the || and ?: operators). Named types can // circularly reference themselves and therefore cannot be subtype reduced during their declaration. // For example, "type Item = string | (() => Item" is a named type that circularly references itself. - function getUnionType(types: Type[], subtypeReduction?: boolean, aliasSymbol?: Symbol, aliasTypeArguments?: Type[]): Type { + function getUnionType(types: Type[], subtypeReduction?: boolean, aliasSymbol?: Symbol, aliasTypeArguments?: Type[], retainRedundantTypes?: boolean): Type { if (types.length === 0) { return neverType; } @@ -7492,7 +7492,7 @@ namespace ts { if (subtypeReduction) { removeSubtypes(typeSet); } - else if (typeSet.containsStringOrNumberLiteral) { + else if (typeSet.containsStringOrNumberLiteral && !retainRedundantTypes) { removeRedundantLiteralTypes(typeSet); } if (typeSet.length === 0) { @@ -11606,7 +11606,7 @@ namespace ts { // Apply a mapping function to a type and return the resulting type. If the source type // is a union type, the mapping function is applied to each constituent type and a union // of the resulting types is returned. - function mapType(type: Type, mapper: (t: Type) => Type): Type { + function mapType(type: Type, mapper: (t: Type) => Type, retainRedundantTypes?: boolean): Type { if (!(type.flags & TypeFlags.Union)) { return mapper(type); } @@ -11627,7 +11627,7 @@ namespace ts { } } } - return mappedTypes ? getUnionType(mappedTypes) : mappedType; + return mappedTypes ? getUnionType(mappedTypes, /*subtypeReduction*/ undefined, /*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined, retainRedundantTypes) : mappedType; } function extractTypesOfKind(type: Type, kind: TypeFlags) { @@ -13149,7 +13149,31 @@ namespace ts { // We have an object literal method. Check if the containing object literal has a contextual type // that includes a ThisType. If so, T is the contextual type for 'this'. We continue looking in // any directly enclosing object literals. - const contextualType = getApparentTypeOfContextualType(containingLiteral); + let contextualType = getApparentTypeOfContextualType(containingLiteral); + if (contextualType && contextualType.flags & TypeFlags.Union) { + let match: Type | undefined; + propLoop: for (const prop of containingLiteral.properties) { + if (!prop.symbol) continue; + if (prop.kind !== SyntaxKind.PropertyAssignment) continue; + if (isDiscriminantProperty(contextualType, prop.symbol.escapedName)) { + const discriminatingType = getTypeOfNode((prop as PropertyAssignment).initializer); + for (const type of (contextualType as UnionType).types) { + const targetType = getTypeOfPropertyOfType(type, prop.symbol.escapedName); + if (targetType && checkTypeAssignableTo(discriminatingType, targetType, /*errorNode*/ undefined)) { + if (match) { + if (type === match) continue; // Finding multiple fields which discriminate to the same type is fine + match = undefined; + break propLoop; + } + match = type; + } + } + } + } + if (match) { + contextualType = match; + } + } let literal = containingLiteral; let type = contextualType; while (type) { @@ -13410,7 +13434,7 @@ namespace ts { return mapType(type, t => { const prop = t.flags & TypeFlags.StructuredType ? getPropertyOfType(t, name) : undefined; return prop ? getTypeOfSymbol(prop) : undefined; - }); + }, /*retainRedundantTypes*/ true); } function getIndexTypeOfContextualType(type: Type, kind: IndexKind) { diff --git a/tests/baselines/reference/contextualTypeShouldBeLiteral.js b/tests/baselines/reference/contextualTypeShouldBeLiteral.js new file mode 100644 index 0000000000000..dcb9da993c1cb --- /dev/null +++ b/tests/baselines/reference/contextualTypeShouldBeLiteral.js @@ -0,0 +1,136 @@ +//// [contextualTypeShouldBeLiteral.ts] +interface X { + type: 'x'; + value: string; + method(): void; +} + +interface Y { + type: 'y'; + value: 'none' | 'done'; + method(): void; +} + +function foo(bar: X | Y) { } + +foo({ + type: 'y', + value: 'done', + method() { + this; + this.type; + this.value; + } +}); + +interface X2 { + type1: 'x'; + value: string; + method(): void; +} + +interface Y2 { + type2: 'y'; + value: 'none' | 'done'; + method(): void; +} + +function foo2(bar: X2 | Y2) { } + +foo2({ + type2: 'y', + value: 'done', + method() { + this; + this.value; + } +}); + +interface X3 { + type: 'x'; + value: 1 | 2 | 3; + xtra: number; +} + +interface Y3 { + type: 'y'; + value: 11 | 12 | 13; + ytra: number; +} + +let xy: X3 | Y3 = { + type: 'y', + value: 11, + ytra: 12 +}; + +xy; + + +interface LikeA { + x: 'x'; + y: 'y'; + value: string; + method(): void; +} + +interface LikeB { + x: 'xx'; + y: 'yy'; + value: number; + method(): void; +} + +let xyz: LikeA | LikeB = { + x: 'x', + y: 'y', + value: "foo", + method() { + this; + this.x; + this.y; + this.value; + } +}; + +xyz; + +//// [contextualTypeShouldBeLiteral.js] +"use strict"; +function foo(bar) { } +foo({ + type: 'y', + value: 'done', + method: function () { + this; + this.type; + this.value; + } +}); +function foo2(bar) { } +foo2({ + type2: 'y', + value: 'done', + method: function () { + this; + this.value; + } +}); +var xy = { + type: 'y', + value: 11, + ytra: 12 +}; +xy; +var xyz = { + x: 'x', + y: 'y', + value: "foo", + method: function () { + this; + this.x; + this.y; + this.value; + } +}; +xyz; diff --git a/tests/baselines/reference/contextualTypeShouldBeLiteral.symbols b/tests/baselines/reference/contextualTypeShouldBeLiteral.symbols new file mode 100644 index 0000000000000..314117b509d94 --- /dev/null +++ b/tests/baselines/reference/contextualTypeShouldBeLiteral.symbols @@ -0,0 +1,229 @@ +=== tests/cases/compiler/contextualTypeShouldBeLiteral.ts === +interface X { +>X : Symbol(X, Decl(contextualTypeShouldBeLiteral.ts, 0, 0)) + + type: 'x'; +>type : Symbol(X.type, Decl(contextualTypeShouldBeLiteral.ts, 0, 13)) + + value: string; +>value : Symbol(X.value, Decl(contextualTypeShouldBeLiteral.ts, 1, 14)) + + method(): void; +>method : Symbol(X.method, Decl(contextualTypeShouldBeLiteral.ts, 2, 18)) +} + +interface Y { +>Y : Symbol(Y, Decl(contextualTypeShouldBeLiteral.ts, 4, 1)) + + type: 'y'; +>type : Symbol(Y.type, Decl(contextualTypeShouldBeLiteral.ts, 6, 13)) + + value: 'none' | 'done'; +>value : Symbol(Y.value, Decl(contextualTypeShouldBeLiteral.ts, 7, 14)) + + method(): void; +>method : Symbol(Y.method, Decl(contextualTypeShouldBeLiteral.ts, 8, 27)) +} + +function foo(bar: X | Y) { } +>foo : Symbol(foo, Decl(contextualTypeShouldBeLiteral.ts, 10, 1)) +>bar : Symbol(bar, Decl(contextualTypeShouldBeLiteral.ts, 12, 13)) +>X : Symbol(X, Decl(contextualTypeShouldBeLiteral.ts, 0, 0)) +>Y : Symbol(Y, Decl(contextualTypeShouldBeLiteral.ts, 4, 1)) + +foo({ +>foo : Symbol(foo, Decl(contextualTypeShouldBeLiteral.ts, 10, 1)) + + type: 'y', +>type : Symbol(type, Decl(contextualTypeShouldBeLiteral.ts, 14, 5)) + + value: 'done', +>value : Symbol(value, Decl(contextualTypeShouldBeLiteral.ts, 15, 14)) + + method() { +>method : Symbol(method, Decl(contextualTypeShouldBeLiteral.ts, 16, 18)) + + this; +>this : Symbol(Y, Decl(contextualTypeShouldBeLiteral.ts, 4, 1)) + + this.type; +>this.type : Symbol(Y.type, Decl(contextualTypeShouldBeLiteral.ts, 6, 13)) +>this : Symbol(Y, Decl(contextualTypeShouldBeLiteral.ts, 4, 1)) +>type : Symbol(Y.type, Decl(contextualTypeShouldBeLiteral.ts, 6, 13)) + + this.value; +>this.value : Symbol(Y.value, Decl(contextualTypeShouldBeLiteral.ts, 7, 14)) +>this : Symbol(Y, Decl(contextualTypeShouldBeLiteral.ts, 4, 1)) +>value : Symbol(Y.value, Decl(contextualTypeShouldBeLiteral.ts, 7, 14)) + } +}); + +interface X2 { +>X2 : Symbol(X2, Decl(contextualTypeShouldBeLiteral.ts, 22, 3)) + + type1: 'x'; +>type1 : Symbol(X2.type1, Decl(contextualTypeShouldBeLiteral.ts, 24, 14)) + + value: string; +>value : Symbol(X2.value, Decl(contextualTypeShouldBeLiteral.ts, 25, 15)) + + method(): void; +>method : Symbol(X2.method, Decl(contextualTypeShouldBeLiteral.ts, 26, 18)) +} + +interface Y2 { +>Y2 : Symbol(Y2, Decl(contextualTypeShouldBeLiteral.ts, 28, 1)) + + type2: 'y'; +>type2 : Symbol(Y2.type2, Decl(contextualTypeShouldBeLiteral.ts, 30, 14)) + + value: 'none' | 'done'; +>value : Symbol(Y2.value, Decl(contextualTypeShouldBeLiteral.ts, 31, 15)) + + method(): void; +>method : Symbol(Y2.method, Decl(contextualTypeShouldBeLiteral.ts, 32, 27)) +} + +function foo2(bar: X2 | Y2) { } +>foo2 : Symbol(foo2, Decl(contextualTypeShouldBeLiteral.ts, 34, 1)) +>bar : Symbol(bar, Decl(contextualTypeShouldBeLiteral.ts, 36, 14)) +>X2 : Symbol(X2, Decl(contextualTypeShouldBeLiteral.ts, 22, 3)) +>Y2 : Symbol(Y2, Decl(contextualTypeShouldBeLiteral.ts, 28, 1)) + +foo2({ +>foo2 : Symbol(foo2, Decl(contextualTypeShouldBeLiteral.ts, 34, 1)) + + type2: 'y', +>type2 : Symbol(type2, Decl(contextualTypeShouldBeLiteral.ts, 38, 6)) + + value: 'done', +>value : Symbol(value, Decl(contextualTypeShouldBeLiteral.ts, 39, 15)) + + method() { +>method : Symbol(method, Decl(contextualTypeShouldBeLiteral.ts, 40, 18)) + + this; + this.value; +>this.value : Symbol(value, Decl(contextualTypeShouldBeLiteral.ts, 25, 15), Decl(contextualTypeShouldBeLiteral.ts, 31, 15)) +>value : Symbol(value, Decl(contextualTypeShouldBeLiteral.ts, 25, 15), Decl(contextualTypeShouldBeLiteral.ts, 31, 15)) + } +}); + +interface X3 { +>X3 : Symbol(X3, Decl(contextualTypeShouldBeLiteral.ts, 45, 3)) + + type: 'x'; +>type : Symbol(X3.type, Decl(contextualTypeShouldBeLiteral.ts, 47, 14)) + + value: 1 | 2 | 3; +>value : Symbol(X3.value, Decl(contextualTypeShouldBeLiteral.ts, 48, 14)) + + xtra: number; +>xtra : Symbol(X3.xtra, Decl(contextualTypeShouldBeLiteral.ts, 49, 21)) +} + +interface Y3 { +>Y3 : Symbol(Y3, Decl(contextualTypeShouldBeLiteral.ts, 51, 1)) + + type: 'y'; +>type : Symbol(Y3.type, Decl(contextualTypeShouldBeLiteral.ts, 53, 14)) + + value: 11 | 12 | 13; +>value : Symbol(Y3.value, Decl(contextualTypeShouldBeLiteral.ts, 54, 14)) + + ytra: number; +>ytra : Symbol(Y3.ytra, Decl(contextualTypeShouldBeLiteral.ts, 55, 24)) +} + +let xy: X3 | Y3 = { +>xy : Symbol(xy, Decl(contextualTypeShouldBeLiteral.ts, 59, 3)) +>X3 : Symbol(X3, Decl(contextualTypeShouldBeLiteral.ts, 45, 3)) +>Y3 : Symbol(Y3, Decl(contextualTypeShouldBeLiteral.ts, 51, 1)) + + type: 'y', +>type : Symbol(type, Decl(contextualTypeShouldBeLiteral.ts, 59, 19)) + + value: 11, +>value : Symbol(value, Decl(contextualTypeShouldBeLiteral.ts, 60, 14)) + + ytra: 12 +>ytra : Symbol(ytra, Decl(contextualTypeShouldBeLiteral.ts, 61, 14)) + +}; + +xy; +>xy : Symbol(xy, Decl(contextualTypeShouldBeLiteral.ts, 59, 3)) + + +interface LikeA { +>LikeA : Symbol(LikeA, Decl(contextualTypeShouldBeLiteral.ts, 65, 3)) + + x: 'x'; +>x : Symbol(LikeA.x, Decl(contextualTypeShouldBeLiteral.ts, 68, 17)) + + y: 'y'; +>y : Symbol(LikeA.y, Decl(contextualTypeShouldBeLiteral.ts, 69, 11)) + + value: string; +>value : Symbol(LikeA.value, Decl(contextualTypeShouldBeLiteral.ts, 70, 11)) + + method(): void; +>method : Symbol(LikeA.method, Decl(contextualTypeShouldBeLiteral.ts, 71, 18)) +} + +interface LikeB { +>LikeB : Symbol(LikeB, Decl(contextualTypeShouldBeLiteral.ts, 73, 1)) + + x: 'xx'; +>x : Symbol(LikeB.x, Decl(contextualTypeShouldBeLiteral.ts, 75, 17)) + + y: 'yy'; +>y : Symbol(LikeB.y, Decl(contextualTypeShouldBeLiteral.ts, 76, 12)) + + value: number; +>value : Symbol(LikeB.value, Decl(contextualTypeShouldBeLiteral.ts, 77, 12)) + + method(): void; +>method : Symbol(LikeB.method, Decl(contextualTypeShouldBeLiteral.ts, 78, 18)) +} + +let xyz: LikeA | LikeB = { +>xyz : Symbol(xyz, Decl(contextualTypeShouldBeLiteral.ts, 82, 3)) +>LikeA : Symbol(LikeA, Decl(contextualTypeShouldBeLiteral.ts, 65, 3)) +>LikeB : Symbol(LikeB, Decl(contextualTypeShouldBeLiteral.ts, 73, 1)) + + x: 'x', +>x : Symbol(x, Decl(contextualTypeShouldBeLiteral.ts, 82, 26)) + + y: 'y', +>y : Symbol(y, Decl(contextualTypeShouldBeLiteral.ts, 83, 11)) + + value: "foo", +>value : Symbol(value, Decl(contextualTypeShouldBeLiteral.ts, 84, 11)) + + method() { +>method : Symbol(method, Decl(contextualTypeShouldBeLiteral.ts, 85, 17)) + + this; +>this : Symbol(LikeA, Decl(contextualTypeShouldBeLiteral.ts, 65, 3)) + + this.x; +>this.x : Symbol(LikeA.x, Decl(contextualTypeShouldBeLiteral.ts, 68, 17)) +>this : Symbol(LikeA, Decl(contextualTypeShouldBeLiteral.ts, 65, 3)) +>x : Symbol(LikeA.x, Decl(contextualTypeShouldBeLiteral.ts, 68, 17)) + + this.y; +>this.y : Symbol(LikeA.y, Decl(contextualTypeShouldBeLiteral.ts, 69, 11)) +>this : Symbol(LikeA, Decl(contextualTypeShouldBeLiteral.ts, 65, 3)) +>y : Symbol(LikeA.y, Decl(contextualTypeShouldBeLiteral.ts, 69, 11)) + + this.value; +>this.value : Symbol(LikeA.value, Decl(contextualTypeShouldBeLiteral.ts, 70, 11)) +>this : Symbol(LikeA, Decl(contextualTypeShouldBeLiteral.ts, 65, 3)) +>value : Symbol(LikeA.value, Decl(contextualTypeShouldBeLiteral.ts, 70, 11)) + } +}; + +xyz; +>xyz : Symbol(xyz, Decl(contextualTypeShouldBeLiteral.ts, 82, 3)) + diff --git a/tests/baselines/reference/contextualTypeShouldBeLiteral.types b/tests/baselines/reference/contextualTypeShouldBeLiteral.types new file mode 100644 index 0000000000000..8ab09df66b832 --- /dev/null +++ b/tests/baselines/reference/contextualTypeShouldBeLiteral.types @@ -0,0 +1,248 @@ +=== tests/cases/compiler/contextualTypeShouldBeLiteral.ts === +interface X { +>X : X + + type: 'x'; +>type : "x" + + value: string; +>value : string + + method(): void; +>method : () => void +} + +interface Y { +>Y : Y + + type: 'y'; +>type : "y" + + value: 'none' | 'done'; +>value : "none" | "done" + + method(): void; +>method : () => void +} + +function foo(bar: X | Y) { } +>foo : (bar: X | Y) => void +>bar : X | Y +>X : X +>Y : Y + +foo({ +>foo({ type: 'y', value: 'done', method() { this; this.type; this.value; }}) : void +>foo : (bar: X | Y) => void +>{ type: 'y', value: 'done', method() { this; this.type; this.value; }} : { type: "y"; value: "done"; method(): void; } + + type: 'y', +>type : string +>'y' : "y" + + value: 'done', +>value : string +>'done' : "done" + + method() { +>method : () => void + + this; +>this : Y + + this.type; +>this.type : "y" +>this : Y +>type : "y" + + this.value; +>this.value : "none" | "done" +>this : Y +>value : "none" | "done" + } +}); + +interface X2 { +>X2 : X2 + + type1: 'x'; +>type1 : "x" + + value: string; +>value : string + + method(): void; +>method : () => void +} + +interface Y2 { +>Y2 : Y2 + + type2: 'y'; +>type2 : "y" + + value: 'none' | 'done'; +>value : "none" | "done" + + method(): void; +>method : () => void +} + +function foo2(bar: X2 | Y2) { } +>foo2 : (bar: X2 | Y2) => void +>bar : X2 | Y2 +>X2 : X2 +>Y2 : Y2 + +foo2({ +>foo2({ type2: 'y', value: 'done', method() { this; this.value; }}) : void +>foo2 : (bar: X2 | Y2) => void +>{ type2: 'y', value: 'done', method() { this; this.value; }} : { type2: "y"; value: "done"; method(): void; } + + type2: 'y', +>type2 : string +>'y' : "y" + + value: 'done', +>value : string +>'done' : "done" + + method() { +>method : () => void + + this; +>this : X2 | Y2 + + this.value; +>this.value : string +>this : X2 | Y2 +>value : string + } +}); + +interface X3 { +>X3 : X3 + + type: 'x'; +>type : "x" + + value: 1 | 2 | 3; +>value : 1 | 2 | 3 + + xtra: number; +>xtra : number +} + +interface Y3 { +>Y3 : Y3 + + type: 'y'; +>type : "y" + + value: 11 | 12 | 13; +>value : 11 | 12 | 13 + + ytra: number; +>ytra : number +} + +let xy: X3 | Y3 = { +>xy : X3 | Y3 +>X3 : X3 +>Y3 : Y3 +>{ type: 'y', value: 11, ytra: 12} : { type: "y"; value: 11; ytra: number; } + + type: 'y', +>type : string +>'y' : "y" + + value: 11, +>value : number +>11 : 11 + + ytra: 12 +>ytra : number +>12 : 12 + +}; + +xy; +>xy : Y3 + + +interface LikeA { +>LikeA : LikeA + + x: 'x'; +>x : "x" + + y: 'y'; +>y : "y" + + value: string; +>value : string + + method(): void; +>method : () => void +} + +interface LikeB { +>LikeB : LikeB + + x: 'xx'; +>x : "xx" + + y: 'yy'; +>y : "yy" + + value: number; +>value : number + + method(): void; +>method : () => void +} + +let xyz: LikeA | LikeB = { +>xyz : LikeA | LikeB +>LikeA : LikeA +>LikeB : LikeB +>{ x: 'x', y: 'y', value: "foo", method() { this; this.x; this.y; this.value; }} : { x: "x"; y: "y"; value: string; method(): void; } + + x: 'x', +>x : string +>'x' : "x" + + y: 'y', +>y : string +>'y' : "y" + + value: "foo", +>value : string +>"foo" : "foo" + + method() { +>method : () => void + + this; +>this : LikeA + + this.x; +>this.x : "x" +>this : LikeA +>x : "x" + + this.y; +>this.y : "y" +>this : LikeA +>y : "y" + + this.value; +>this.value : string +>this : LikeA +>value : string + } +}; + +xyz; +>xyz : LikeA + diff --git a/tests/cases/compiler/contextualTypeShouldBeLiteral.ts b/tests/cases/compiler/contextualTypeShouldBeLiteral.ts new file mode 100644 index 0000000000000..2a2deda50ab62 --- /dev/null +++ b/tests/cases/compiler/contextualTypeShouldBeLiteral.ts @@ -0,0 +1,97 @@ + +// @strict: true +interface X { + type: 'x'; + value: string; + method(): void; +} + +interface Y { + type: 'y'; + value: 'none' | 'done'; + method(): void; +} + +function foo(bar: X | Y) { } + +foo({ + type: 'y', + value: 'done', + method() { + this; + this.type; + this.value; + } +}); + +interface X2 { + type1: 'x'; + value: string; + method(): void; +} + +interface Y2 { + type2: 'y'; + value: 'none' | 'done'; + method(): void; +} + +function foo2(bar: X2 | Y2) { } + +foo2({ + type2: 'y', + value: 'done', + method() { + this; + this.value; + } +}); + +interface X3 { + type: 'x'; + value: 1 | 2 | 3; + xtra: number; +} + +interface Y3 { + type: 'y'; + value: 11 | 12 | 13; + ytra: number; +} + +let xy: X3 | Y3 = { + type: 'y', + value: 11, + ytra: 12 +}; + +xy; + + +interface LikeA { + x: 'x'; + y: 'y'; + value: string; + method(): void; +} + +interface LikeB { + x: 'xx'; + y: 'yy'; + value: number; + method(): void; +} + +let xyz: LikeA | LikeB = { + x: 'x', + y: 'y', + value: "foo", + method() { + this; + this.x; + this.y; + this.value; + } +}; + +xyz; \ No newline at end of file