Skip to content
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

Discriminate contextual types #19733

Merged
merged 5 commits into from
Nov 7, 2017
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
59 changes: 44 additions & 15 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9269,20 +9269,24 @@ namespace ts {
return Ternary.False;
}

// Keep this up-to-date with the same logic within `getApparentTypeOfContextualType`, since they should behave similarly
function findMatchingDiscriminantType(source: Type, target: UnionOrIntersectionType) {
let match: Type;
const sourceProperties = getPropertiesOfObjectType(source);
if (sourceProperties) {
const sourceProperty = findSingleDiscriminantProperty(sourceProperties, target);
if (sourceProperty) {
const sourceType = getTypeOfSymbol(sourceProperty);
for (const type of target.types) {
const targetType = getTypeOfPropertyOfType(type, sourceProperty.escapedName);
if (targetType && isRelatedTo(sourceType, targetType)) {
if (match) {
return undefined;
const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target);
if (sourcePropertiesFiltered) {
for (const sourceProperty of sourcePropertiesFiltered) {
const sourceType = getTypeOfSymbol(sourceProperty);
for (const type of target.types) {
const targetType = getTypeOfPropertyOfType(type, sourceProperty.escapedName);
if (targetType && isRelatedTo(sourceType, targetType)) {
if (type === match) continue; // Finding multiple fields which discriminate to the same type is fine
if (match) {
return undefined;
}
match = type;
}
match = type;
}
}
}
Expand Down Expand Up @@ -11396,14 +11400,15 @@ namespace ts {
return false;
}

function findSingleDiscriminantProperty(sourceProperties: Symbol[], target: Type): Symbol | undefined {
let result: Symbol;
function findDiscriminantProperties(sourceProperties: Symbol[], target: Type): Symbol[] | undefined {
let result: Symbol[];
for (const sourceProperty of sourceProperties) {
if (isDiscriminantProperty(target, sourceProperty.escapedName)) {
if (result) {
return undefined;
result.push(sourceProperty);
continue;
}
result = sourceProperty;
result = [sourceProperty];
}
}
return result;
Expand Down Expand Up @@ -13691,8 +13696,32 @@ 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 {
const type = getContextualType(node);
return type && mapType(type, getApparentType);
let contextualType = getContextualType(node);
contextualType = contextualType && mapType(contextualType, getApparentType);
if (!(contextualType && contextualType.flags & TypeFlags.Union && isObjectLiteralExpression(node))) {
return contextualType;
}
// Keep the below up-to-date with the work done within `isRelatedTo` by `findMatchingDiscriminantType`
let match: Type | undefined;
propLoop: for (const prop of node.properties) {
if (!prop.symbol) continue;
if (prop.kind !== SyntaxKind.PropertyAssignment) continue;
if (isDiscriminantProperty(contextualType, prop.symbol.escapedName)) {
const discriminatingType = getTypeOfNode(prop.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;
}
}
}
}
return match || contextualType;
}

/**
Expand Down
42 changes: 42 additions & 0 deletions tests/baselines/reference/contextuallyTypedByDiscriminableUnion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//// [contextuallyTypedByDiscriminableUnion.ts]
type ADT = {
kind: "a",
method(x: string): number;
} | {
kind: "b",
method(x: number): string;
};


function invoke(item: ADT) {
if (item.kind === "a") {
item.method("");
}
else {
item.method(42);
}
}

invoke({
kind: "a",
method(a) {
return +a;
}
});


//// [contextuallyTypedByDiscriminableUnion.js]
function invoke(item) {
if (item.kind === "a") {
item.method("");
}
else {
item.method(42);
}
}
invoke({
kind: "a",
method: function (a) {
return +a;
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
=== tests/cases/compiler/contextuallyTypedByDiscriminableUnion.ts ===
type ADT = {
>ADT : Symbol(ADT, Decl(contextuallyTypedByDiscriminableUnion.ts, 0, 0))

kind: "a",
>kind : Symbol(kind, Decl(contextuallyTypedByDiscriminableUnion.ts, 0, 12))

method(x: string): number;
>method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 1, 14))
>x : Symbol(x, Decl(contextuallyTypedByDiscriminableUnion.ts, 2, 11))

} | {
kind: "b",
>kind : Symbol(kind, Decl(contextuallyTypedByDiscriminableUnion.ts, 3, 5))

method(x: number): string;
>method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 4, 14))
>x : Symbol(x, Decl(contextuallyTypedByDiscriminableUnion.ts, 5, 11))

};


function invoke(item: ADT) {
>invoke : Symbol(invoke, Decl(contextuallyTypedByDiscriminableUnion.ts, 6, 2))
>item : Symbol(item, Decl(contextuallyTypedByDiscriminableUnion.ts, 9, 16))
>ADT : Symbol(ADT, Decl(contextuallyTypedByDiscriminableUnion.ts, 0, 0))

if (item.kind === "a") {
>item.kind : Symbol(kind, Decl(contextuallyTypedByDiscriminableUnion.ts, 0, 12), Decl(contextuallyTypedByDiscriminableUnion.ts, 3, 5))
>item : Symbol(item, Decl(contextuallyTypedByDiscriminableUnion.ts, 9, 16))
>kind : Symbol(kind, Decl(contextuallyTypedByDiscriminableUnion.ts, 0, 12), Decl(contextuallyTypedByDiscriminableUnion.ts, 3, 5))

item.method("");
>item.method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 1, 14))
>item : Symbol(item, Decl(contextuallyTypedByDiscriminableUnion.ts, 9, 16))
>method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 1, 14))
}
else {
item.method(42);
>item.method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 4, 14))
>item : Symbol(item, Decl(contextuallyTypedByDiscriminableUnion.ts, 9, 16))
>method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 4, 14))
}
}

invoke({
>invoke : Symbol(invoke, Decl(contextuallyTypedByDiscriminableUnion.ts, 6, 2))

kind: "a",
>kind : Symbol(kind, Decl(contextuallyTypedByDiscriminableUnion.ts, 18, 8))

method(a) {
>method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 19, 14))
>a : Symbol(a, Decl(contextuallyTypedByDiscriminableUnion.ts, 20, 11))

return +a;
>a : Symbol(a, Decl(contextuallyTypedByDiscriminableUnion.ts, 20, 11))
}
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
=== tests/cases/compiler/contextuallyTypedByDiscriminableUnion.ts ===
type ADT = {
>ADT : ADT

kind: "a",
>kind : "a"

method(x: string): number;
>method : (x: string) => number
>x : string

} | {
kind: "b",
>kind : "b"

method(x: number): string;
>method : (x: number) => string
>x : number

};


function invoke(item: ADT) {
>invoke : (item: ADT) => void
>item : ADT
>ADT : ADT

if (item.kind === "a") {
>item.kind === "a" : boolean
>item.kind : "a" | "b"
>item : ADT
>kind : "a" | "b"
>"a" : "a"

item.method("");
>item.method("") : number
>item.method : (x: string) => number
>item : { kind: "a"; method(x: string): number; }
>method : (x: string) => number
>"" : ""
}
else {
item.method(42);
>item.method(42) : string
>item.method : (x: number) => string
>item : { kind: "b"; method(x: number): string; }
>method : (x: number) => string
>42 : 42
}
}

invoke({
>invoke({ kind: "a", method(a) { return +a; }}) : void
>invoke : (item: ADT) => void
>{ kind: "a", method(a) { return +a; }} : { kind: "a"; method(a: string): number; }

kind: "a",
>kind : string
>"a" : "a"

method(a) {
>method : (a: string) => number
>a : string

return +a;
>+a : number
>a : string
}
});

18 changes: 14 additions & 4 deletions tests/baselines/reference/excessPropertyCheckWithUnions.errors.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
tests/cases/compiler/excessPropertyCheckWithUnions.ts(10,30): error TS2322: Type '{ tag: "T"; a1: string; }' is not assignable to type 'ADT'.
Object literal may only specify known properties, and 'a1' does not exist in type '{ tag: "T"; }'.
tests/cases/compiler/excessPropertyCheckWithUnions.ts(11,21): error TS2322: Type '{ tag: "A"; d20: 12; }' is not assignable to type 'ADT'.
tests/cases/compiler/excessPropertyCheckWithUnions.ts(11,21): error TS2322: Type '{ tag: "A"; d20: number; }' is not assignable to type 'ADT'.
Object literal may only specify known properties, and 'd20' does not exist in type '{ tag: "A"; a1: string; }'.
tests/cases/compiler/excessPropertyCheckWithUnions.ts(12,1): error TS2322: Type '{ tag: "D"; }' is not assignable to type 'ADT'.
Type '{ tag: "D"; }' is not assignable to type '{ tag: "D"; d20: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20; }'.
Expand All @@ -17,9 +17,13 @@ tests/cases/compiler/excessPropertyCheckWithUnions.ts(40,1): error TS2322: Type
Type '{ tag: "A"; z: true; }' is not assignable to type '{ tag: "C"; }'.
Types of property 'tag' are incompatible.
Type '"A"' is not assignable to type '"C"'.
tests/cases/compiler/excessPropertyCheckWithUnions.ts(49,35): error TS2322: Type '{ a: 1; b: 1; first: string; second: string; }' is not assignable to type 'Overlapping'.
Object literal may only specify known properties, and 'second' does not exist in type '{ a: 1; b: 1; first: string; }'.
tests/cases/compiler/excessPropertyCheckWithUnions.ts(50,35): error TS2322: Type '{ a: 1; b: 1; first: string; third: string; }' is not assignable to type 'Overlapping'.
Object literal may only specify known properties, and 'third' does not exist in type '{ a: 1; b: 1; first: string; }'.


==== tests/cases/compiler/excessPropertyCheckWithUnions.ts (7 errors) ====
==== tests/cases/compiler/excessPropertyCheckWithUnions.ts (9 errors) ====
type ADT = {
tag: "A",
a1: string
Expand All @@ -35,7 +39,7 @@ tests/cases/compiler/excessPropertyCheckWithUnions.ts(40,1): error TS2322: Type
!!! error TS2322: Object literal may only specify known properties, and 'a1' does not exist in type '{ tag: "T"; }'.
wrong = { tag: "A", d20: 12 }
~~~~~~~
!!! error TS2322: Type '{ tag: "A"; d20: 12; }' is not assignable to type 'ADT'.
!!! error TS2322: Type '{ tag: "A"; d20: number; }' is not assignable to type 'ADT'.
!!! error TS2322: Object literal may only specify known properties, and 'd20' does not exist in type '{ tag: "A"; a1: string; }'.
wrong = { tag: "D" }
~~~~~
Expand Down Expand Up @@ -93,9 +97,15 @@ tests/cases/compiler/excessPropertyCheckWithUnions.ts(40,1): error TS2322: Type
| { b: 3, third: string }
let over: Overlapping

// these two are not reported because there are two discriminant properties
// these two are still errors despite their doubled up discriminants
over = { a: 1, b: 1, first: "ok", second: "error" }
~~~~~~~~~~~~~~~
!!! error TS2322: Type '{ a: 1; b: 1; first: string; second: string; }' is not assignable to type 'Overlapping'.
!!! error TS2322: Object literal may only specify known properties, and 'second' does not exist in type '{ a: 1; b: 1; first: string; }'.
over = { a: 1, b: 1, first: "ok", third: "error" }
~~~~~~~~~~~~~~
!!! error TS2322: Type '{ a: 1; b: 1; first: string; third: string; }' is not assignable to type 'Overlapping'.
!!! error TS2322: Object literal may only specify known properties, and 'third' does not exist in type '{ a: 1; b: 1; first: string; }'.

// Freshness disappears after spreading a union
declare let t0: { a: any, b: any } | { d: any, e: any }
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/excessPropertyCheckWithUnions.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type Overlapping =
| { b: 3, third: string }
let over: Overlapping

// these two are not reported because there are two discriminant properties
// these two are still errors despite their doubled up discriminants
over = { a: 1, b: 1, first: "ok", second: "error" }
over = { a: 1, b: 1, first: "ok", third: "error" }

Expand Down Expand Up @@ -84,7 +84,7 @@ amb = { tag: "A", y: 12, extra: 12 };
amb = { tag: "A" };
amb = { tag: "A", z: true };
var over;
// these two are not reported because there are two discriminant properties
// these two are still errors despite their doubled up discriminants
over = { a: 1, b: 1, first: "ok", second: "error" };
over = { a: 1, b: 1, first: "ok", third: "error" };
var t2 = __assign({}, t1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ let over: Overlapping
>over : Symbol(over, Decl(excessPropertyCheckWithUnions.ts, 45, 3))
>Overlapping : Symbol(Overlapping, Decl(excessPropertyCheckWithUnions.ts, 39, 27))

// these two are not reported because there are two discriminant properties
// these two are still errors despite their doubled up discriminants
over = { a: 1, b: 1, first: "ok", second: "error" }
>over : Symbol(over, Decl(excessPropertyCheckWithUnions.ts, 45, 3))
>a : Symbol(a, Decl(excessPropertyCheckWithUnions.ts, 48, 8))
Expand Down
6 changes: 3 additions & 3 deletions tests/baselines/reference/excessPropertyCheckWithUnions.types
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ let wrong: ADT = { tag: "T", a1: "extra" }
>"extra" : "extra"

wrong = { tag: "A", d20: 12 }
>wrong = { tag: "A", d20: 12 } : { tag: "A"; d20: 12; }
>wrong = { tag: "A", d20: 12 } : { tag: "A"; d20: number; }
>wrong : ADT
>{ tag: "A", d20: 12 } : { tag: "A"; d20: 12; }
>{ tag: "A", d20: 12 } : { tag: "A"; d20: number; }
>tag : string
>"A" : "A"
>d20 : number
Expand Down Expand Up @@ -167,7 +167,7 @@ let over: Overlapping
>over : Overlapping
>Overlapping : Overlapping

// these two are not reported because there are two discriminant properties
// these two are still errors despite their doubled up discriminants
over = { a: 1, b: 1, first: "ok", second: "error" }
>over = { a: 1, b: 1, first: "ok", second: "error" } : { a: 1; b: 1; first: string; second: string; }
>over : Overlapping
Expand Down
25 changes: 25 additions & 0 deletions tests/cases/compiler/contextuallyTypedByDiscriminableUnion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// @noImplicitAny: true
type ADT = {
kind: "a",
method(x: string): number;
} | {
kind: "b",
method(x: number): string;
};


function invoke(item: ADT) {
if (item.kind === "a") {
item.method("");
}
else {
item.method(42);
}
}

invoke({
kind: "a",
method(a) {
return +a;
}
});
2 changes: 1 addition & 1 deletion tests/cases/compiler/excessPropertyCheckWithUnions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type Overlapping =
| { b: 3, third: string }
let over: Overlapping

// these two are not reported because there are two discriminant properties
// these two are still errors despite their doubled up discriminants
over = { a: 1, b: 1, first: "ok", second: "error" }
over = { a: 1, b: 1, first: "ok", third: "error" }

Expand Down