Skip to content

Commit

Permalink
Merge pull request #29847 from Microsoft/inferToUnionTypes
Browse files Browse the repository at this point in the history
Improve inference to union and intersection types
  • Loading branch information
ahejlsberg authored Feb 11, 2019
2 parents d66000b + ce6c04e commit f93f4f3
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 38 deletions.
25 changes: 7 additions & 18 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14473,26 +14473,15 @@ namespace ts {
inferFromTypes(source, getUnionType([getTrueTypeFromConditionalType(<ConditionalType>target), getFalseTypeFromConditionalType(<ConditionalType>target)]));
}
else if (target.flags & TypeFlags.UnionOrIntersection) {
const targetTypes = (<UnionOrIntersectionType>target).types;
let typeVariableCount = 0;
let typeVariable: TypeParameter | IndexedAccessType | undefined;
// First infer to each type in union or intersection that isn't a type variable
for (const t of targetTypes) {
for (const t of (<UnionOrIntersectionType>target).types) {
const savePriority = priority;
// Inferences directly to naked type variables are given lower priority as they are
// less specific. For example, when inferring from Promise<string> to T | Promise<T>,
// we want to infer string for T, not Promise<string> | string.
if (getInferenceInfoForType(t)) {
typeVariable = <InstantiableType>t;
typeVariableCount++;
}
else {
inferFromTypes(source, t);
priority |= InferencePriority.NakedTypeVariable;
}
}
// Next, if target containings a single naked type variable, make a secondary inference to that type
// variable. This gives meaningful results for union types in co-variant positions and intersection
// types in contra-variant positions (such as callback parameters).
if (typeVariableCount === 1) {
const savePriority = priority;
priority |= InferencePriority.NakedTypeVariable;
inferFromTypes(source, typeVariable!);
inferFromTypes(source, t);
priority = savePriority;
}
}
Expand Down
16 changes: 8 additions & 8 deletions tests/baselines/reference/conditionalTypeDoesntSpinForever.types
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ export enum PubSubRecordIsStoredInRedisAsA {

buildPubSubRecordType(Object.assign({}, soFar, {storedAs: PubSubRecordIsStoredInRedisAsA.jsonEncodedRedisString})) as
>buildPubSubRecordType(Object.assign({}, soFar, {storedAs: PubSubRecordIsStoredInRedisAsA.jsonEncodedRedisString})) as BuildPubSubRecordType<SO_FAR & {storedAs: PubSubRecordIsStoredInRedisAsA.jsonEncodedRedisString}> : BuildPubSubRecordType<SO_FAR & { storedAs: PubSubRecordIsStoredInRedisAsA.jsonEncodedRedisString; }>
>buildPubSubRecordType(Object.assign({}, soFar, {storedAs: PubSubRecordIsStoredInRedisAsA.jsonEncodedRedisString})) : BuildPubSubRecordType<SO_FAR & { storedAs: PubSubRecordIsStoredInRedisAsA; }>
>buildPubSubRecordType(Object.assign({}, soFar, {storedAs: PubSubRecordIsStoredInRedisAsA.jsonEncodedRedisString})) : BuildPubSubRecordType<SO_FAR & { storedAs: PubSubRecordIsStoredInRedisAsA.jsonEncodedRedisString; }>
>buildPubSubRecordType : <SO_FAR>(soFar: SO_FAR) => BuildPubSubRecordType<SO_FAR>
>Object.assign({}, soFar, {storedAs: PubSubRecordIsStoredInRedisAsA.jsonEncodedRedisString}) : SO_FAR & { storedAs: PubSubRecordIsStoredInRedisAsA; }
>Object.assign({}, soFar, {storedAs: PubSubRecordIsStoredInRedisAsA.jsonEncodedRedisString}) : SO_FAR & { storedAs: PubSubRecordIsStoredInRedisAsA.jsonEncodedRedisString; }
>Object.assign : { <T, U>(target: T, source: U): T & U; <T, U, V>(target: T, source1: U, source2: V): T & U & V; <T, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W; (target: object, ...sources: any[]): any; }
>Object : ObjectConstructor
>assign : { <T, U>(target: T, source: U): T & U; <T, U, V>(target: T, source1: U, source2: V): T & U & V; <T, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W; (target: object, ...sources: any[]): any; }
Expand All @@ -144,9 +144,9 @@ export enum PubSubRecordIsStoredInRedisAsA {

buildPubSubRecordType(Object.assign({}, soFar, {storedAs: PubSubRecordIsStoredInRedisAsA.redisHash})) as
>buildPubSubRecordType(Object.assign({}, soFar, {storedAs: PubSubRecordIsStoredInRedisAsA.redisHash})) as BuildPubSubRecordType<SO_FAR & {storedAs: PubSubRecordIsStoredInRedisAsA.redisHash}> : BuildPubSubRecordType<SO_FAR & { storedAs: PubSubRecordIsStoredInRedisAsA.redisHash; }>
>buildPubSubRecordType(Object.assign({}, soFar, {storedAs: PubSubRecordIsStoredInRedisAsA.redisHash})) : BuildPubSubRecordType<SO_FAR & { storedAs: PubSubRecordIsStoredInRedisAsA; }>
>buildPubSubRecordType(Object.assign({}, soFar, {storedAs: PubSubRecordIsStoredInRedisAsA.redisHash})) : BuildPubSubRecordType<SO_FAR & { storedAs: PubSubRecordIsStoredInRedisAsA.redisHash; }>
>buildPubSubRecordType : <SO_FAR>(soFar: SO_FAR) => BuildPubSubRecordType<SO_FAR>
>Object.assign({}, soFar, {storedAs: PubSubRecordIsStoredInRedisAsA.redisHash}) : SO_FAR & { storedAs: PubSubRecordIsStoredInRedisAsA; }
>Object.assign({}, soFar, {storedAs: PubSubRecordIsStoredInRedisAsA.redisHash}) : SO_FAR & { storedAs: PubSubRecordIsStoredInRedisAsA.redisHash; }
>Object.assign : { <T, U>(target: T, source: U): T & U; <T, U, V>(target: T, source1: U, source2: V): T & U & V; <T, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W; (target: object, ...sources: any[]): any; }
>Object : ObjectConstructor
>assign : { <T, U>(target: T, source: U): T & U; <T, U, V>(target: T, source1: U, source2: V): T & U & V; <T, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W; (target: object, ...sources: any[]): any; }
Expand Down Expand Up @@ -337,16 +337,16 @@ export enum PubSubRecordIsStoredInRedisAsA {

buildPubSubRecordType(Object.assign({}, soFar, {maxMsToWaitBeforePublishing: 0})) as BuildPubSubRecordType<SO_FAR & {maxMsToWaitBeforePublishing: 0}>,
>buildPubSubRecordType(Object.assign({}, soFar, {maxMsToWaitBeforePublishing: 0})) as BuildPubSubRecordType<SO_FAR & {maxMsToWaitBeforePublishing: 0}> : BuildPubSubRecordType<SO_FAR & { maxMsToWaitBeforePublishing: 0; }>
>buildPubSubRecordType(Object.assign({}, soFar, {maxMsToWaitBeforePublishing: 0})) : BuildPubSubRecordType<SO_FAR & { maxMsToWaitBeforePublishing: number; }>
>buildPubSubRecordType(Object.assign({}, soFar, {maxMsToWaitBeforePublishing: 0})) : BuildPubSubRecordType<SO_FAR & { maxMsToWaitBeforePublishing: 0; }>
>buildPubSubRecordType : <SO_FAR>(soFar: SO_FAR) => BuildPubSubRecordType<SO_FAR>
>Object.assign({}, soFar, {maxMsToWaitBeforePublishing: 0}) : SO_FAR & { maxMsToWaitBeforePublishing: number; }
>Object.assign({}, soFar, {maxMsToWaitBeforePublishing: 0}) : SO_FAR & { maxMsToWaitBeforePublishing: 0; }
>Object.assign : { <T, U>(target: T, source: U): T & U; <T, U, V>(target: T, source1: U, source2: V): T & U & V; <T, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W; (target: object, ...sources: any[]): any; }
>Object : ObjectConstructor
>assign : { <T, U>(target: T, source: U): T & U; <T, U, V>(target: T, source1: U, source2: V): T & U & V; <T, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W; (target: object, ...sources: any[]): any; }
>{} : {}
>soFar : SO_FAR
>{maxMsToWaitBeforePublishing: 0} : { maxMsToWaitBeforePublishing: number; }
>maxMsToWaitBeforePublishing : number
>{maxMsToWaitBeforePublishing: 0} : { maxMsToWaitBeforePublishing: 0; }
>maxMsToWaitBeforePublishing : 0
>0 : 0
>maxMsToWaitBeforePublishing : 0
}
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/jqueryInference.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ declare function shouldBeIdentity<T, U>(p: DoNothingAlias<T, U>): MyPromise<T, U

declare const p1: MyPromise<boolean, any>;
var p2 = shouldBeIdentity(p1);
var p2: MyPromise<boolean, {}>;
var p2: MyPromise<boolean, any>;


//// [jqueryInference.js]
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/jqueryInference.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ var p2 = shouldBeIdentity(p1);
>shouldBeIdentity : Symbol(shouldBeIdentity, Decl(jqueryInference.ts, 6, 58))
>p1 : Symbol(p1, Decl(jqueryInference.ts, 10, 13))

var p2: MyPromise<boolean, {}>;
var p2: MyPromise<boolean, any>;
>p2 : Symbol(p2, Decl(jqueryInference.ts, 11, 3), Decl(jqueryInference.ts, 12, 3))
>MyPromise : Symbol(MyPromise, Decl(jqueryInference.ts, 0, 0))

8 changes: 4 additions & 4 deletions tests/baselines/reference/jqueryInference.types
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ declare const p1: MyPromise<boolean, any>;
>p1 : MyPromise<boolean, any>

var p2 = shouldBeIdentity(p1);
>p2 : MyPromise<boolean, {}>
>shouldBeIdentity(p1) : MyPromise<boolean, {}>
>p2 : MyPromise<boolean, any>
>shouldBeIdentity(p1) : MyPromise<boolean, any>
>shouldBeIdentity : <T, U>(p: DoNothingAlias<T, U>) => MyPromise<T, U>
>p1 : MyPromise<boolean, any>

var p2: MyPromise<boolean, {}>;
>p2 : MyPromise<boolean, {}>
var p2: MyPromise<boolean, any>;
>p2 : MyPromise<boolean, any>

2 changes: 1 addition & 1 deletion tests/baselines/reference/objectSpread.types
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ let exclusive: { id: string, a: number, b: string, c: string, d: boolean } =
>d : boolean

f({ a: 1, b: 'yes' }, { c: 'no', d: false })
>f({ a: 1, b: 'yes' }, { c: 'no', d: false }) : { a: number; b: string; } & { c: string; d: boolean; } & { id: string; }
>f({ a: 1, b: 'yes' }, { c: 'no', d: false }) : { a: number; b: string; } & { c: string; d: false; } & { id: string; }
>f : <T, U>(t: T, u: U) => T & U & { id: string; }
>{ a: 1, b: 'yes' } : { a: number; b: string; }
>a : number
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/restTupleElements1.types
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ f0([]); // Error
>[] : never[]

f0([1]);
>f0([1]) : [number, {}]
>f0([1]) : [number, number]
>f0 : <T, U>(x: [T, ...U[]]) => [T, U]
>[1] : [number]
>1 : 1
Expand Down
28 changes: 26 additions & 2 deletions tests/baselines/reference/unionAndIntersectionInference1.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ declare var mbp: Man & Bear;

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

// Repros from #29815

interface ITest {
name: 'test'
}

const createTestAsync = (): Promise<ITest> => Promise.resolve().then(() => ({ name: 'test' }))

const createTest = (): ITest => {
return { name: 'test' }
}

declare function f1<T, U>(x: T | U): T | U;
declare function f2<T, U>(x: T & U): T & U;

let x1: string = f1('a');
let x2: string = f2('a');


//// [unionAndIntersectionInference1.js]
Expand All @@ -80,7 +98,7 @@ 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
var result = destructure(value, text => 'string', y => 'other one'); // text: string, y: Y
// Repro from #4212
function isVoid(value) {
return undefined;
Expand All @@ -107,7 +125,13 @@ function baz1(value) {
function get(x) {
return null; // just an example
}
var foo;
let foo;
get(foo).toUpperCase(); // Ok
pigify(mbp).oinks; // OK, mbp is treated as Pig
pigify(mbp).walks; // Ok, mbp is treated as Man
const createTestAsync = () => Promise.resolve().then(() => ({ name: 'test' }));
const createTest = () => {
return { name: 'test' };
};
let x1 = f1('a');
let x2 = f2('a');
58 changes: 57 additions & 1 deletion tests/baselines/reference/unionAndIntersectionInference1.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function destructure<a, r>(
var value = Math.random() > 0.5 ? 'hey!' : <Y>undefined;
>value : Symbol(value, Decl(unionAndIntersectionInference1.ts, 12, 3))
>Math.random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --))
>Math : Symbol(Math, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Math : Symbol(Math, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --))
>random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --))
>Y : Symbol(Y, Decl(unionAndIntersectionInference1.ts, 0, 0))
>undefined : Symbol(undefined)
Expand Down Expand Up @@ -201,3 +201,59 @@ pigify(mbp).walks; // Ok, mbp is treated as Man
>mbp : Symbol(mbp, Decl(unionAndIntersectionInference1.ts, 68, 11))
>walks : Symbol(Man.walks, Decl(unionAndIntersectionInference1.ts, 55, 15))

// Repros from #29815

interface ITest {
>ITest : Symbol(ITest, Decl(unionAndIntersectionInference1.ts, 71, 18))

name: 'test'
>name : Symbol(ITest.name, Decl(unionAndIntersectionInference1.ts, 75, 17))
}

const createTestAsync = (): Promise<ITest> => Promise.resolve().then(() => ({ name: 'test' }))
>createTestAsync : Symbol(createTestAsync, Decl(unionAndIntersectionInference1.ts, 79, 5))
>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --))
>ITest : Symbol(ITest, Decl(unionAndIntersectionInference1.ts, 71, 18))
>Promise.resolve().then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --))
>Promise.resolve : Symbol(PromiseConstructor.resolve, Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --))
>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --))
>resolve : Symbol(PromiseConstructor.resolve, Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --))
>then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --))
>name : Symbol(name, Decl(unionAndIntersectionInference1.ts, 79, 77))

const createTest = (): ITest => {
>createTest : Symbol(createTest, Decl(unionAndIntersectionInference1.ts, 81, 5))
>ITest : Symbol(ITest, Decl(unionAndIntersectionInference1.ts, 71, 18))

return { name: 'test' }
>name : Symbol(name, Decl(unionAndIntersectionInference1.ts, 82, 10))
}

declare function f1<T, U>(x: T | U): T | U;
>f1 : Symbol(f1, Decl(unionAndIntersectionInference1.ts, 83, 1))
>T : Symbol(T, Decl(unionAndIntersectionInference1.ts, 85, 20))
>U : Symbol(U, Decl(unionAndIntersectionInference1.ts, 85, 22))
>x : Symbol(x, Decl(unionAndIntersectionInference1.ts, 85, 26))
>T : Symbol(T, Decl(unionAndIntersectionInference1.ts, 85, 20))
>U : Symbol(U, Decl(unionAndIntersectionInference1.ts, 85, 22))
>T : Symbol(T, Decl(unionAndIntersectionInference1.ts, 85, 20))
>U : Symbol(U, Decl(unionAndIntersectionInference1.ts, 85, 22))

declare function f2<T, U>(x: T & U): T & U;
>f2 : Symbol(f2, Decl(unionAndIntersectionInference1.ts, 85, 43))
>T : Symbol(T, Decl(unionAndIntersectionInference1.ts, 86, 20))
>U : Symbol(U, Decl(unionAndIntersectionInference1.ts, 86, 22))
>x : Symbol(x, Decl(unionAndIntersectionInference1.ts, 86, 26))
>T : Symbol(T, Decl(unionAndIntersectionInference1.ts, 86, 20))
>U : Symbol(U, Decl(unionAndIntersectionInference1.ts, 86, 22))
>T : Symbol(T, Decl(unionAndIntersectionInference1.ts, 86, 20))
>U : Symbol(U, Decl(unionAndIntersectionInference1.ts, 86, 22))

let x1: string = f1('a');
>x1 : Symbol(x1, Decl(unionAndIntersectionInference1.ts, 88, 3))
>f1 : Symbol(f1, Decl(unionAndIntersectionInference1.ts, 83, 1))

let x2: string = f2('a');
>x2 : Symbol(x2, Decl(unionAndIntersectionInference1.ts, 89, 3))
>f2 : Symbol(f2, Decl(unionAndIntersectionInference1.ts, 85, 43))

53 changes: 53 additions & 0 deletions tests/baselines/reference/unionAndIntersectionInference1.types
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,56 @@ pigify(mbp).walks; // Ok, mbp is treated as Man
>mbp : Man & Bear
>walks : boolean

// Repros from #29815

interface ITest {
name: 'test'
>name : "test"
}

const createTestAsync = (): Promise<ITest> => Promise.resolve().then(() => ({ name: 'test' }))
>createTestAsync : () => Promise<ITest>
>(): Promise<ITest> => Promise.resolve().then(() => ({ name: 'test' })) : () => Promise<ITest>
>Promise.resolve().then(() => ({ name: 'test' })) : Promise<ITest | { name: "test"; }>
>Promise.resolve().then : <TResult1 = void, TResult2 = never>(onfulfilled?: (value: void) => TResult1 | PromiseLike<TResult1>, onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>) => Promise<TResult1 | TResult2>
>Promise.resolve() : Promise<void>
>Promise.resolve : { <T>(value: T | PromiseLike<T>): Promise<T>; (): Promise<void>; }
>Promise : PromiseConstructor
>resolve : { <T>(value: T | PromiseLike<T>): Promise<T>; (): Promise<void>; }
>then : <TResult1 = void, TResult2 = never>(onfulfilled?: (value: void) => TResult1 | PromiseLike<TResult1>, onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>) => Promise<TResult1 | TResult2>
>() => ({ name: 'test' }) : () => { name: "test"; }
>({ name: 'test' }) : { name: "test"; }
>{ name: 'test' } : { name: "test"; }
>name : "test"
>'test' : "test"

const createTest = (): ITest => {
>createTest : () => ITest
>(): ITest => { return { name: 'test' }} : () => ITest

return { name: 'test' }
>{ name: 'test' } : { name: "test"; }
>name : "test"
>'test' : "test"
}

declare function f1<T, U>(x: T | U): T | U;
>f1 : <T, U>(x: T | U) => T | U
>x : T | U

declare function f2<T, U>(x: T & U): T & U;
>f2 : <T, U>(x: T & U) => T & U
>x : T & U

let x1: string = f1('a');
>x1 : string
>f1('a') : "a"
>f1 : <T, U>(x: T | U) => T | U
>'a' : "a"

let x2: string = f2('a');
>x2 : string
>f2('a') : "a"
>f2 : <T, U>(x: T & U) => T & U
>'a' : "a"

2 changes: 1 addition & 1 deletion tests/cases/compiler/jqueryInference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ declare function shouldBeIdentity<T, U>(p: DoNothingAlias<T, U>): MyPromise<T, U

declare const p1: MyPromise<boolean, any>;
var p2 = shouldBeIdentity(p1);
var p2: MyPromise<boolean, {}>;
var p2: MyPromise<boolean, any>;
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @target: es2015

// Repro from #2264

interface Y { 'i am a very certain type': Y }
Expand Down Expand Up @@ -70,3 +72,21 @@ declare var mbp: Man & Bear;

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

// Repros from #29815

interface ITest {
name: 'test'
}

const createTestAsync = (): Promise<ITest> => Promise.resolve().then(() => ({ name: 'test' }))

const createTest = (): ITest => {
return { name: 'test' }
}

declare function f1<T, U>(x: T | U): T | U;
declare function f2<T, U>(x: T & U): T & U;

let x1: string = f1('a');
let x2: string = f2('a');

0 comments on commit f93f4f3

Please sign in to comment.