Skip to content

Retain literal types in some contextual unions #19322

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

Closed
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
36 changes: 30 additions & 6 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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<T>. 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) {
Expand Down Expand Up @@ -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) {
Expand Down
136 changes: 136 additions & 0 deletions tests/baselines/reference/contextualTypeShouldBeLiteral.js
Original file line number Diff line number Diff line change
@@ -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;
Loading