Skip to content

Commit 456806b

Browse files
authored
Allow filterType to consider union constraints of non-union types when determining never-ness (#43763)
* Allow filterType to consider union constraints of non-union types when determining never-ness * Move impl to callback * Baseline change in narrowing behavior into test, fix post-LKG build
1 parent b83148f commit 456806b

7 files changed

+935
-3
lines changed

src/compiler/checker.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23819,7 +23819,16 @@ namespace ts {
2381923819

2382023820
function getNarrowedType(type: Type, candidate: Type, assumeTrue: boolean, isRelated: (source: Type, target: Type) => boolean) {
2382123821
if (!assumeTrue) {
23822-
return filterType(type, t => !isRelated(t, candidate));
23822+
return filterType(type, t => {
23823+
if (!isRelated(t, candidate)) {
23824+
return true;
23825+
}
23826+
const constraint = getBaseConstraintOfType(t);
23827+
if (constraint && constraint !== t) {
23828+
return !isRelated(constraint, candidate);
23829+
}
23830+
return false;
23831+
});
2382323832
}
2382423833
// If the current type is a union type, remove all constituents that couldn't be instances of
2382523834
// the candidate type. If one or more constituents remain, return a union of those.

src/compiler/watch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ namespace ts {
113113
return `${newLine}${flattenDiagnosticMessageText(d.messageText, newLine)}${newLine}${newLine}`;
114114
}
115115

116-
export function isBuilderProgram<T extends BuilderProgram>(program: Program | T): program is T {
117-
return !!(program as T).getState;
116+
export function isBuilderProgram(program: Program | BuilderProgram): program is BuilderProgram {
117+
return !!(program as BuilderProgram).getState;
118118
}
119119

120120
export function listFiles<T extends BuilderProgram>(program: Program | T, write: (s: string) => void) {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
tests/cases/compiler/quickinfoTypeAtReturnPositionsInaccurate.ts(33,15): error TS2339: Property 'numExclusive' does not exist on type 'NumClass<number> | StrClass<string>'.
2+
Property 'numExclusive' does not exist on type 'StrClass<string>'.
3+
tests/cases/compiler/quickinfoTypeAtReturnPositionsInaccurate.ts(101,11): error TS2322: Type 'Program | T' is not assignable to type 'Program'.
4+
Type 'T' is not assignable to type 'Program'.
5+
Property 'state' is missing in type 'BuilderProgram' but required in type 'Program'.
6+
7+
8+
==== tests/cases/compiler/quickinfoTypeAtReturnPositionsInaccurate.ts (2 errors) ====
9+
class NumClass<T extends number> {
10+
private value!: T;
11+
public get(): T {
12+
return this.value;
13+
}
14+
public numExclusive() { }
15+
}
16+
17+
class StrClass<T extends string> {
18+
private value!: T;
19+
public get(): T {
20+
return this.value;
21+
}
22+
public strExclusive() { }
23+
}
24+
25+
const isNumClass = <Item extends NumClass<number> | StrClass<string>> (
26+
item: Item
27+
): item is Extract<Item, NumClass<any>> => {
28+
return (item instanceof NumClass);
29+
}
30+
31+
/**
32+
* An example with one dimensional dictionary. Everything worked ok here, even in prior
33+
* versions.
34+
*/
35+
class SimpleStore<Entries extends { [index: string]: NumClass<number> | StrClass<string> }> {
36+
private entries = { } as Entries;
37+
38+
public get<EntryId extends keyof Entries>(entryId: EntryId): Entries[EntryId] {
39+
let entry = this.entries[entryId];
40+
41+
entry.numExclusive(); // error - expected.
42+
~~~~~~~~~~~~
43+
!!! error TS2339: Property 'numExclusive' does not exist on type 'NumClass<number> | StrClass<string>'.
44+
!!! error TS2339: Property 'numExclusive' does not exist on type 'StrClass<string>'.
45+
46+
if (isNumClass(entry)) {
47+
entry.numExclusive(); // works
48+
return entry;
49+
}
50+
51+
return entry; // type is Entries[EntryId] - all fine
52+
}
53+
}
54+
55+
type Slice = {
56+
[index: string]: NumClass<number> | StrClass<string>
57+
}
58+
59+
/**
60+
* A an example with 2-dimensional dictionary.
61+
*
62+
* In v4.1 the `isNumClass` type guard doesn't work at all.
63+
* In v4.2 or later, `isNumClass` type guard leaks outside its
64+
* scope.
65+
*/
66+
class ComplexStore<Slices extends { [index: string]: Slice }> {
67+
private slices = { } as Slices;
68+
69+
public get<SliceId extends keyof Slices, SliceKey extends keyof Slices[SliceId]>(
70+
sliceId: SliceId, sliceKey: SliceKey
71+
): Slices[SliceId][SliceKey] {
72+
let item = this.slices[sliceId][sliceKey];
73+
74+
if (isNumClass(item)) {
75+
item.numExclusive(); // works only since version 4.2
76+
}
77+
78+
item.get();
79+
80+
// unfortunately, doesn't work completely.
81+
// it seems like item's predicated type leaks outside the bracket...
82+
83+
return item; // type is Extract ...
84+
}
85+
86+
public get2<SliceId extends keyof Slices, SliceKey extends keyof Slices[SliceId]>(
87+
sliceId: SliceId, sliceKey: SliceKey
88+
): Slices[SliceId][SliceKey] {
89+
let item = this.slices[sliceId][sliceKey];
90+
91+
if (isNumClass(item)) {
92+
return item;
93+
}
94+
// it seems like the compiler asumes the above condition is always
95+
// truthy
96+
97+
item.get();
98+
99+
return item; // type is never
100+
}
101+
}
102+
103+
// from the compiler itself
104+
interface BuilderProgram {
105+
getProgram(): Program;
106+
}
107+
interface Program {
108+
state: any;
109+
}
110+
declare function isBuilderProgram<T extends BuilderProgram>(program: Program | T): program is T;
111+
export function listFiles<T extends BuilderProgram>(program: Program | T) {
112+
const x: Program = isBuilderProgram(program) ? program.getProgram() : program;
113+
~
114+
!!! error TS2322: Type 'Program | T' is not assignable to type 'Program'.
115+
!!! error TS2322: Type 'T' is not assignable to type 'Program'.
116+
!!! error TS2322: Property 'state' is missing in type 'BuilderProgram' but required in type 'Program'.
117+
!!! related TS2728 tests/cases/compiler/quickinfoTypeAtReturnPositionsInaccurate.ts:97:5: 'state' is declared here.
118+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
//// [quickinfoTypeAtReturnPositionsInaccurate.ts]
2+
class NumClass<T extends number> {
3+
private value!: T;
4+
public get(): T {
5+
return this.value;
6+
}
7+
public numExclusive() { }
8+
}
9+
10+
class StrClass<T extends string> {
11+
private value!: T;
12+
public get(): T {
13+
return this.value;
14+
}
15+
public strExclusive() { }
16+
}
17+
18+
const isNumClass = <Item extends NumClass<number> | StrClass<string>> (
19+
item: Item
20+
): item is Extract<Item, NumClass<any>> => {
21+
return (item instanceof NumClass);
22+
}
23+
24+
/**
25+
* An example with one dimensional dictionary. Everything worked ok here, even in prior
26+
* versions.
27+
*/
28+
class SimpleStore<Entries extends { [index: string]: NumClass<number> | StrClass<string> }> {
29+
private entries = { } as Entries;
30+
31+
public get<EntryId extends keyof Entries>(entryId: EntryId): Entries[EntryId] {
32+
let entry = this.entries[entryId];
33+
34+
entry.numExclusive(); // error - expected.
35+
36+
if (isNumClass(entry)) {
37+
entry.numExclusive(); // works
38+
return entry;
39+
}
40+
41+
return entry; // type is Entries[EntryId] - all fine
42+
}
43+
}
44+
45+
type Slice = {
46+
[index: string]: NumClass<number> | StrClass<string>
47+
}
48+
49+
/**
50+
* A an example with 2-dimensional dictionary.
51+
*
52+
* In v4.1 the `isNumClass` type guard doesn't work at all.
53+
* In v4.2 or later, `isNumClass` type guard leaks outside its
54+
* scope.
55+
*/
56+
class ComplexStore<Slices extends { [index: string]: Slice }> {
57+
private slices = { } as Slices;
58+
59+
public get<SliceId extends keyof Slices, SliceKey extends keyof Slices[SliceId]>(
60+
sliceId: SliceId, sliceKey: SliceKey
61+
): Slices[SliceId][SliceKey] {
62+
let item = this.slices[sliceId][sliceKey];
63+
64+
if (isNumClass(item)) {
65+
item.numExclusive(); // works only since version 4.2
66+
}
67+
68+
item.get();
69+
70+
// unfortunately, doesn't work completely.
71+
// it seems like item's predicated type leaks outside the bracket...
72+
73+
return item; // type is Extract ...
74+
}
75+
76+
public get2<SliceId extends keyof Slices, SliceKey extends keyof Slices[SliceId]>(
77+
sliceId: SliceId, sliceKey: SliceKey
78+
): Slices[SliceId][SliceKey] {
79+
let item = this.slices[sliceId][sliceKey];
80+
81+
if (isNumClass(item)) {
82+
return item;
83+
}
84+
// it seems like the compiler asumes the above condition is always
85+
// truthy
86+
87+
item.get();
88+
89+
return item; // type is never
90+
}
91+
}
92+
93+
// from the compiler itself
94+
interface BuilderProgram {
95+
getProgram(): Program;
96+
}
97+
interface Program {
98+
state: any;
99+
}
100+
declare function isBuilderProgram<T extends BuilderProgram>(program: Program | T): program is T;
101+
export function listFiles<T extends BuilderProgram>(program: Program | T) {
102+
const x: Program = isBuilderProgram(program) ? program.getProgram() : program;
103+
}
104+
105+
//// [quickinfoTypeAtReturnPositionsInaccurate.js]
106+
"use strict";
107+
exports.__esModule = true;
108+
exports.listFiles = void 0;
109+
var NumClass = /** @class */ (function () {
110+
function NumClass() {
111+
}
112+
NumClass.prototype.get = function () {
113+
return this.value;
114+
};
115+
NumClass.prototype.numExclusive = function () { };
116+
return NumClass;
117+
}());
118+
var StrClass = /** @class */ (function () {
119+
function StrClass() {
120+
}
121+
StrClass.prototype.get = function () {
122+
return this.value;
123+
};
124+
StrClass.prototype.strExclusive = function () { };
125+
return StrClass;
126+
}());
127+
var isNumClass = function (item) {
128+
return (item instanceof NumClass);
129+
};
130+
/**
131+
* An example with one dimensional dictionary. Everything worked ok here, even in prior
132+
* versions.
133+
*/
134+
var SimpleStore = /** @class */ (function () {
135+
function SimpleStore() {
136+
this.entries = {};
137+
}
138+
SimpleStore.prototype.get = function (entryId) {
139+
var entry = this.entries[entryId];
140+
entry.numExclusive(); // error - expected.
141+
if (isNumClass(entry)) {
142+
entry.numExclusive(); // works
143+
return entry;
144+
}
145+
return entry; // type is Entries[EntryId] - all fine
146+
};
147+
return SimpleStore;
148+
}());
149+
/**
150+
* A an example with 2-dimensional dictionary.
151+
*
152+
* In v4.1 the `isNumClass` type guard doesn't work at all.
153+
* In v4.2 or later, `isNumClass` type guard leaks outside its
154+
* scope.
155+
*/
156+
var ComplexStore = /** @class */ (function () {
157+
function ComplexStore() {
158+
this.slices = {};
159+
}
160+
ComplexStore.prototype.get = function (sliceId, sliceKey) {
161+
var item = this.slices[sliceId][sliceKey];
162+
if (isNumClass(item)) {
163+
item.numExclusive(); // works only since version 4.2
164+
}
165+
item.get();
166+
// unfortunately, doesn't work completely.
167+
// it seems like item's predicated type leaks outside the bracket...
168+
return item; // type is Extract ...
169+
};
170+
ComplexStore.prototype.get2 = function (sliceId, sliceKey) {
171+
var item = this.slices[sliceId][sliceKey];
172+
if (isNumClass(item)) {
173+
return item;
174+
}
175+
// it seems like the compiler asumes the above condition is always
176+
// truthy
177+
item.get();
178+
return item; // type is never
179+
};
180+
return ComplexStore;
181+
}());
182+
function listFiles(program) {
183+
var x = isBuilderProgram(program) ? program.getProgram() : program;
184+
}
185+
exports.listFiles = listFiles;

0 commit comments

Comments
 (0)