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

Improved union/intersection type inference #5738

Merged
merged 4 commits into from
Nov 24, 2015
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
47 changes: 46 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6086,6 +6086,17 @@ namespace ts {
}

function inferFromTypes(source: Type, target: Type) {
if (source.flags & TypeFlags.Union && target.flags & TypeFlags.Union ||
source.flags & TypeFlags.Intersection && target.flags & TypeFlags.Intersection) {
// Source and target are both unions or both intersections. To improve the quality of
// inferences we first reduce the types by removing constituents that are identically
// matched by a constituent in the other type. For example, when inferring from
// 'string | string[]' to 'string | T', we reduce the types to 'string[]' and 'T'.
const reducedSource = reduceUnionOrIntersectionType(<UnionOrIntersectionType>source, <UnionOrIntersectionType>target);
const reducedTarget = reduceUnionOrIntersectionType(<UnionOrIntersectionType>target, <UnionOrIntersectionType>source);
source = reducedSource;
target = reducedTarget;
}
if (target.flags & TypeFlags.TypeParameter) {
// If target is a type parameter, make an inference, unless the source type contains
// the anyFunctionType (the wildcard type that's used to avoid contextually typing functions).
Expand All @@ -6096,7 +6107,6 @@ namespace ts {
if (source.flags & TypeFlags.ContainsAnyFunctionType) {
return;
}

const typeParameters = context.typeParameters;
for (let i = 0; i < typeParameters.length; i++) {
if (target === typeParameters[i]) {
Expand Down Expand Up @@ -6244,6 +6254,41 @@ namespace ts {
}
}

function typeIdenticalToSomeType(source: Type, target: UnionOrIntersectionType): boolean {
Copy link
Member

Choose a reason for hiding this comment

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

How about

return forEach(target.types, t => isTypeIdenticalTo(source, t) || undefined);

Copy link
Member Author

Choose a reason for hiding this comment

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

Definitely an option, but not quite as efficient. I will keep what's there.

for (const t of target.types) {
if (isTypeIdenticalTo(source, t)) {
return true;
}
}
return false;
}

/**
* Return the reduced form of the source type. This type is computed by by removing all source
* constituents that have an identical match in the target type.
*/
function reduceUnionOrIntersectionType(source: UnionOrIntersectionType, target: UnionOrIntersectionType) {
let sourceTypes = source.types;
let sourceIndex = 0;
let modified = false;
while (sourceIndex < sourceTypes.length) {
if (typeIdenticalToSomeType(sourceTypes[sourceIndex], target)) {
if (!modified) {
sourceTypes = sourceTypes.slice(0);
modified = true;
}
sourceTypes.splice(sourceIndex, 1);
Copy link
Member

Choose a reason for hiding this comment

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

I don't think I understand the mutability discipline for the checker. It looks like splice modifies source.types. Is this OK? How do we know that nobody else refers to source? (even though inferFromTypes no longer does after the call to reduceUnionOrIntersection.)

Copy link
Member

Choose a reason for hiding this comment

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

Never mind, I just saw the call to .slice(0) above.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, while it seems obvious now, it'd be worthwhile to add a quick comment so that people taking a cursory glance don't think they've found a bug.

}
else {
sourceIndex++;
}
}
if (modified) {
return source.flags & TypeFlags.Union ? getUnionType(sourceTypes, /*noSubtypeReduction*/ true) : getIntersectionType(sourceTypes);
Copy link
Member

Choose a reason for hiding this comment

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

Can you break this into multiple lines?

Copy link

Choose a reason for hiding this comment

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

Use wordwrapping man.

}
return source;
}

function getInferenceCandidates(context: InferenceContext, index: number): Type[] {
const inferences = context.inferences[index];
return inferences.primary || inferences.secondary || emptyArray;
Expand Down
113 changes: 113 additions & 0 deletions tests/baselines/reference/unionAndIntersectionInference1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//// [unionAndIntersectionInference1.ts]
// Repro from #2264

interface Y { 'i am a very certain type': Y }
var y: Y = <Y>undefined;
function destructure<a, r>(
something: a | Y,
haveValue: (value: a) => r,
haveY: (value: Y) => r
): r {
return something === y ? haveY(y) : haveValue(<a>something);
}

var value = Math.random() > 0.5 ? 'hey!' : <Y>undefined;

var result = destructure(value, text => 'string', y => 'other one'); // text: string, y: Y

// Repro from #4212

function isVoid<a>(value: void | a): value is void {
return undefined;
}

function isNonVoid<a>(value: void | a) : value is a {
return undefined;
}

function foo1<a>(value: void|a): void {
if (isVoid(value)) {
value; // value is void
} else {
value; // value is a
}
}

function baz1<a>(value: void|a): void {
if (isNonVoid(value)) {
value; // value is a
} else {
value; // value is void
}
}

// Repro from #5417

type Maybe<T> = T | void;

function get<U>(x: U | void): U {
return null; // just an example
}

let foo: Maybe<string>;
get(foo).toUpperCase(); // Ok

// Repro from #5456

interface Man {
walks: boolean;
}

interface Bear {
roars: boolean;
}

interface Pig {
oinks: boolean;
}

declare function pigify<T>(y: T & Bear): T & Pig;
declare var mbp: Man & Bear;

pigify(mbp).oinks; // OK, mbp is treated as Pig
pigify(mbp).walks; // Ok, mbp is treated as Man


//// [unionAndIntersectionInference1.js]
// Repro from #2264
var y = undefined;
function destructure(something, haveValue, haveY) {
return something === y ? haveY(y) : haveValue(something);
}
var value = Math.random() > 0.5 ? 'hey!' : undefined;
var result = destructure(value, function (text) { return 'string'; }, function (y) { return 'other one'; }); // text: string, y: Y
// Repro from #4212
function isVoid(value) {
return undefined;
}
function isNonVoid(value) {
return undefined;
}
function foo1(value) {
if (isVoid(value)) {
value; // value is void
}
else {
value; // value is a
}
}
function baz1(value) {
if (isNonVoid(value)) {
value; // value is a
}
else {
value; // value is void
}
}
function get(x) {
return null; // just an example
}
var foo;
get(foo).toUpperCase(); // Ok
pigify(mbp).oinks; // OK, mbp is treated as Pig
pigify(mbp).walks; // Ok, mbp is treated as Man
202 changes: 202 additions & 0 deletions tests/baselines/reference/unionAndIntersectionInference1.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
=== tests/cases/conformance/types/typeRelationships/typeInference/unionAndIntersectionInference1.ts ===
// Repro from #2264

interface Y { 'i am a very certain type': Y }
>Y : Symbol(Y, Decl(unionAndIntersectionInference1.ts, 0, 0))
>Y : Symbol(Y, Decl(unionAndIntersectionInference1.ts, 0, 0))

var y: Y = <Y>undefined;
>y : Symbol(y, Decl(unionAndIntersectionInference1.ts, 3, 3))
>Y : Symbol(Y, Decl(unionAndIntersectionInference1.ts, 0, 0))
>Y : Symbol(Y, Decl(unionAndIntersectionInference1.ts, 0, 0))
>undefined : Symbol(undefined)

function destructure<a, r>(
>destructure : Symbol(destructure, Decl(unionAndIntersectionInference1.ts, 3, 24))
>a : Symbol(a, Decl(unionAndIntersectionInference1.ts, 4, 21))
>r : Symbol(r, Decl(unionAndIntersectionInference1.ts, 4, 23))

something: a | Y,
>something : Symbol(something, Decl(unionAndIntersectionInference1.ts, 4, 27))
>a : Symbol(a, Decl(unionAndIntersectionInference1.ts, 4, 21))
>Y : Symbol(Y, Decl(unionAndIntersectionInference1.ts, 0, 0))

haveValue: (value: a) => r,
>haveValue : Symbol(haveValue, Decl(unionAndIntersectionInference1.ts, 5, 21))
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 6, 16))
>a : Symbol(a, Decl(unionAndIntersectionInference1.ts, 4, 21))
>r : Symbol(r, Decl(unionAndIntersectionInference1.ts, 4, 23))

haveY: (value: Y) => r
>haveY : Symbol(haveY, Decl(unionAndIntersectionInference1.ts, 6, 31))
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 7, 12))
>Y : Symbol(Y, Decl(unionAndIntersectionInference1.ts, 0, 0))
>r : Symbol(r, Decl(unionAndIntersectionInference1.ts, 4, 23))

): r {
>r : Symbol(r, Decl(unionAndIntersectionInference1.ts, 4, 23))

return something === y ? haveY(y) : haveValue(<a>something);
>something : Symbol(something, Decl(unionAndIntersectionInference1.ts, 4, 27))
>y : Symbol(y, Decl(unionAndIntersectionInference1.ts, 3, 3))
>haveY : Symbol(haveY, Decl(unionAndIntersectionInference1.ts, 6, 31))
>y : Symbol(y, Decl(unionAndIntersectionInference1.ts, 3, 3))
>haveValue : Symbol(haveValue, Decl(unionAndIntersectionInference1.ts, 5, 21))
>a : Symbol(a, Decl(unionAndIntersectionInference1.ts, 4, 21))
>something : Symbol(something, Decl(unionAndIntersectionInference1.ts, 4, 27))
}

var value = Math.random() > 0.5 ? 'hey!' : <Y>undefined;
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 12, 3))
>Math.random : Symbol(Math.random, Decl(lib.d.ts, --, --))
>Math : Symbol(Math, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
>random : Symbol(Math.random, Decl(lib.d.ts, --, --))
>Y : Symbol(Y, Decl(unionAndIntersectionInference1.ts, 0, 0))
>undefined : Symbol(undefined)

var result = destructure(value, text => 'string', y => 'other one'); // text: string, y: Y
>result : Symbol(result, Decl(unionAndIntersectionInference1.ts, 14, 3))
>destructure : Symbol(destructure, Decl(unionAndIntersectionInference1.ts, 3, 24))
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 12, 3))
>text : Symbol(text, Decl(unionAndIntersectionInference1.ts, 14, 31))
>y : Symbol(y, Decl(unionAndIntersectionInference1.ts, 14, 49))

// Repro from #4212

function isVoid<a>(value: void | a): value is void {
>isVoid : Symbol(isVoid, Decl(unionAndIntersectionInference1.ts, 14, 68))
>a : Symbol(a, Decl(unionAndIntersectionInference1.ts, 18, 16))
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 18, 19))
>a : Symbol(a, Decl(unionAndIntersectionInference1.ts, 18, 16))
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 18, 19))

return undefined;
>undefined : Symbol(undefined)
}

function isNonVoid<a>(value: void | a) : value is a {
>isNonVoid : Symbol(isNonVoid, Decl(unionAndIntersectionInference1.ts, 20, 1))
>a : Symbol(a, Decl(unionAndIntersectionInference1.ts, 22, 19))
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 22, 22))
>a : Symbol(a, Decl(unionAndIntersectionInference1.ts, 22, 19))
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 22, 22))
>a : Symbol(a, Decl(unionAndIntersectionInference1.ts, 22, 19))

return undefined;
>undefined : Symbol(undefined)
}

function foo1<a>(value: void|a): void {
>foo1 : Symbol(foo1, Decl(unionAndIntersectionInference1.ts, 24, 1))
>a : Symbol(a, Decl(unionAndIntersectionInference1.ts, 26, 14))
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 26, 17))
>a : Symbol(a, Decl(unionAndIntersectionInference1.ts, 26, 14))

if (isVoid(value)) {
>isVoid : Symbol(isVoid, Decl(unionAndIntersectionInference1.ts, 14, 68))
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 26, 17))

value; // value is void
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 26, 17))

} else {
value; // value is a
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 26, 17))
}
}

function baz1<a>(value: void|a): void {
>baz1 : Symbol(baz1, Decl(unionAndIntersectionInference1.ts, 32, 1))
>a : Symbol(a, Decl(unionAndIntersectionInference1.ts, 34, 14))
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 34, 17))
>a : Symbol(a, Decl(unionAndIntersectionInference1.ts, 34, 14))

if (isNonVoid(value)) {
>isNonVoid : Symbol(isNonVoid, Decl(unionAndIntersectionInference1.ts, 20, 1))
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 34, 17))

value; // value is a
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 34, 17))

} else {
value; // value is void
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 34, 17))
}
}

// Repro from #5417

type Maybe<T> = T | void;
>Maybe : Symbol(Maybe, Decl(unionAndIntersectionInference1.ts, 40, 1))
>T : Symbol(T, Decl(unionAndIntersectionInference1.ts, 44, 11))
>T : Symbol(T, Decl(unionAndIntersectionInference1.ts, 44, 11))

function get<U>(x: U | void): U {
>get : Symbol(get, Decl(unionAndIntersectionInference1.ts, 44, 25))
>U : Symbol(U, Decl(unionAndIntersectionInference1.ts, 46, 13))
>x : Symbol(x, Decl(unionAndIntersectionInference1.ts, 46, 16))
>U : Symbol(U, Decl(unionAndIntersectionInference1.ts, 46, 13))
>U : Symbol(U, Decl(unionAndIntersectionInference1.ts, 46, 13))

return null; // just an example
}

let foo: Maybe<string>;
>foo : Symbol(foo, Decl(unionAndIntersectionInference1.ts, 50, 3))
>Maybe : Symbol(Maybe, Decl(unionAndIntersectionInference1.ts, 40, 1))

get(foo).toUpperCase(); // Ok
>get(foo).toUpperCase : Symbol(String.toUpperCase, Decl(lib.d.ts, --, --))
>get : Symbol(get, Decl(unionAndIntersectionInference1.ts, 44, 25))
>foo : Symbol(foo, Decl(unionAndIntersectionInference1.ts, 50, 3))
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.d.ts, --, --))

// Repro from #5456

interface Man {
>Man : Symbol(Man, Decl(unionAndIntersectionInference1.ts, 51, 23))

walks: boolean;
>walks : Symbol(walks, Decl(unionAndIntersectionInference1.ts, 55, 15))
}

interface Bear {
>Bear : Symbol(Bear, Decl(unionAndIntersectionInference1.ts, 57, 1))

roars: boolean;
>roars : Symbol(roars, Decl(unionAndIntersectionInference1.ts, 59, 16))
}

interface Pig {
>Pig : Symbol(Pig, Decl(unionAndIntersectionInference1.ts, 61, 1))

oinks: boolean;
>oinks : Symbol(oinks, Decl(unionAndIntersectionInference1.ts, 63, 15))
}

declare function pigify<T>(y: T & Bear): T & Pig;
>pigify : Symbol(pigify, Decl(unionAndIntersectionInference1.ts, 65, 1))
>T : Symbol(T, Decl(unionAndIntersectionInference1.ts, 67, 24))
>y : Symbol(y, Decl(unionAndIntersectionInference1.ts, 67, 27))
>T : Symbol(T, Decl(unionAndIntersectionInference1.ts, 67, 24))
>Bear : Symbol(Bear, Decl(unionAndIntersectionInference1.ts, 57, 1))
>T : Symbol(T, Decl(unionAndIntersectionInference1.ts, 67, 24))
>Pig : Symbol(Pig, Decl(unionAndIntersectionInference1.ts, 61, 1))

declare var mbp: Man & Bear;
>mbp : Symbol(mbp, Decl(unionAndIntersectionInference1.ts, 68, 11))
>Man : Symbol(Man, Decl(unionAndIntersectionInference1.ts, 51, 23))
>Bear : Symbol(Bear, Decl(unionAndIntersectionInference1.ts, 57, 1))

pigify(mbp).oinks; // OK, mbp is treated as Pig
>pigify(mbp).oinks : Symbol(Pig.oinks, Decl(unionAndIntersectionInference1.ts, 63, 15))
>pigify : Symbol(pigify, Decl(unionAndIntersectionInference1.ts, 65, 1))
>mbp : Symbol(mbp, Decl(unionAndIntersectionInference1.ts, 68, 11))
>oinks : Symbol(Pig.oinks, Decl(unionAndIntersectionInference1.ts, 63, 15))

pigify(mbp).walks; // Ok, mbp is treated as Man
>pigify(mbp).walks : Symbol(Man.walks, Decl(unionAndIntersectionInference1.ts, 55, 15))
>pigify : Symbol(pigify, Decl(unionAndIntersectionInference1.ts, 65, 1))
>mbp : Symbol(mbp, Decl(unionAndIntersectionInference1.ts, 68, 11))
>walks : Symbol(Man.walks, Decl(unionAndIntersectionInference1.ts, 55, 15))

Loading