8000 Merge pull request #29711 from jack-williams/switch-on-unknown · microsoft/TypeScript@1ec8a71 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1ec8a71

Browse files
Merge pull request #29711 from jack-williams/switch-on-unknown
Fix #29710: Narrow unknown in switch
2 parents 2f9218f + 25f9a1f commit 1ec8a71

File tree

6 files changed

+1226
-71
lines changed

6 files changed

+1226
-71
lines changed

src/compiler/checker.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16151,13 +16151,37 @@ namespace ts {
1615116151
}
1615216152

1615316153
function narrowTypeBySwitchOnDiscriminant(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) {
16154-
// We only narrow if all case expressions specify values with unit types
16154+
// We only narrow if all case expressions specify
16155+
// values with unit types, except for the case where
16156+
// `type` is unknown. In this instance we map object
16157+
// types to the nonPrimitive type and narrow with that.
1615516158
const switchTypes = getSwitchClauseTypes(switchStatement);
1615616159
if (!switchTypes.length) {
1615716160
return type;
1615816161
}
1615916162
const clauseTypes = switchTypes.slice(clauseStart, clauseEnd);
1616016163
const hasDefaultClause = clauseStart === clauseEnd || contains(clauseTypes, neverType);
16164+
if ((type.flags & TypeFlags.Unknown) && !hasDefaultClause) {
16165+
let groundClauseTypes: Type[] | undefined;
16166+
for (let i = 0; i < clauseTypes.length; i += 1) {
16167+
const t = clauseTypes[i];
16168+
if (t.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) {
16169+
if (groundClauseTypes !== undefined) {
16170+
groundClauseTypes.push(t);
16171+
}
16172+
}
16173+
else if (t.flags & TypeFlags.Object) {
16174+
if (groundClauseTypes === undefined) {
16175+
groundClauseTypes = clauseTypes.slice(0, i);
16176+
}
16177+
groundClauseTypes.push(nonPrimitiveType);
16178+
}
16179+
else {
16180+
return type;
16181+
}
16182+
}
16183+
return getUnionType(groundClauseTypes === undefined ? clauseTypes : groundClauseTypes);
16184+
}
1616116185
const discriminantType = getUnionType(clauseTypes);
1616216186
const caseType =
1616316187
discriminantType.flags & TypeFlags.Never ? neverType :
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
tests/cases/conformance/types/unknown/unknownType2.ts(216,13): error TS2322: Type '"yes" | "no" | "maybe"' is not assignable to type 'SomeResponse'.
2+
Type '"maybe"' is not assignable to type 'SomeResponse'.
3+
4+
5+
==== tests/cases/conformance/types/unknown/unknownType2.ts (1 errors) ====
6+
type isUnknown<T> = unknown extends T ? true : false;
7+
type isTrue<T extends true> = T;
8+
9+
type SomeResponse = 'yes' | 'no' | 'idk';
10+
let validate: (x: unknown) => SomeResponse = x => (x === 'yes' || x === 'no') ? x : 'idk'; // No error
11+
12+
const u: unknown = undefined;
13+
14+
declare const symb: unique symbol;
15+
declare const symbNonUnique: symbol;
16+
17+
if (u === 5) {
18+
const y = u.toString(10);
19+
}
20+
21+
if (u === true || u === false) {
22+
const someBool: boolean = u;
23+
}
24+
25+
if (u === undefined) {
26+
const undef: undefined = u;
27+
}
28+
29+
if (u === null) {
30+
const someNull: null = u;
31+
}
32+
33+
if (u === symb) {
34+
const symbolAlias: typeof symb = u;
35+
}
36+
37+
if (!(u === 42)) {
38+
type A = isTrue<isUnknown<typeof u>>
39+
}
40+
41+
if (u !== 42) {
42+
type B = isTrue<isUnknown<typeof u>>
43+
}
44+
45+
if (u == 42) {
46+
type C = isTrue<isUnknown<typeof u>>
47+
}
48+
49+
if (u == true) {
50+
type D = isTrue<isUnknown<typeof u>>
51+
}
52+
53+
if (u == Object) {
54+
type E = isTrue<isUnknown<typeof u>>
55+
}
56+
57+
declare const aString: string;
58+
declare const aBoolean: boolean;
59+
declare const aNumber: number;
60+
declare const anObject: object;
61+
declare const anObjectLiteral: { x: number };
62+
declare const aUnion: { x: number } | { y: string };
63+
declare const anIntersection: { x: number } & { y: string };
64+
declare const aFunction: () => number;
65+
66+
if (u === aString) {
67+
let uString: string = u;
68+
}
69+
70+
if (u === aBoolean) {
71+
let uString: boolean = u;
72+
}
73+
74+
if (u === aNumber) {
75+
let uNumber: number = u;
76+
}
77+
78+
if (u === anObject) {
79+
let uObject: object = u;
80+
}
81+
82+
if (u === anObjectLiteral) {
83+
let uObjectLiteral: object = u;
84+
}
85+
86+
if (u === aUnion) {
87+
type unionDoesNotNarrow = isTrue<isUnknown<typeof u>>
88+
}
89+
90+
if (u === anIntersection) {
91+
type intersectionDoesNotNarrow = isTrue<isUnknown<typeof u>>
92+
}
93+
94+
if (u === aFunction) {
95+
let uFunction: object = u;
96+
}
97+
98+
enum NumberEnum {
99+
A,
100+
B,
101+
C
102+
}
103+
104+
enum StringEnum {
105+
A = "A",
106+
B = "B",
107+
C = "C"
108+
}
109+
110+
if (u === NumberEnum || u === StringEnum) {
111+
let enumObj: object = u;
112+
}
113+
114+
if (u === NumberEnum.A) {
1 10000 15+
let a: NumberEnum.A = u
116+
}
117+
118+
if (u === StringEnum.B) {
119+
let b: StringEnum.B = u
120+
}
121+
122+
function switchTestEnum(x: unknown) {
123+
switch (x) {
124+
case StringEnum.A:
125+
const a: StringEnum.A = x;
126+
break;
127+
case StringEnum.B:
128+
const b: StringEnum.B = x;
129+
break;
130+
case StringEnum.C:
131+
const c: StringEnum.C = x;
132+
break;
133+
}
134+
type End = isTrue<isUnknown<typeof x>>
135+
}
136+
137+
function switchTestCollectEnum(x: unknown) {
138+
switch (x) {
139+
case StringEnum.A:
140+
const a: StringEnum.A = x;
141+
case StringEnum.B:
142+
const b: StringEnum.A | StringEnum.B = x;
143+
case StringEnum.C:
144+
const c: StringEnum.A | StringEnum.B | StringEnum.C = x;
145+
const all: StringEnum = x;
146+
return;
147+
}
148+
type End = isTrue<isUnknown<typeof x>>
149+
}
150+
151+
function switchTestLiterals(x: unknown) {
152+
switch (x) {
153+
case 1:
154+
const one: 1 = x;
155+
break;
156+
case 2:
157+
const two: 2 = x;
158+
break;
159+
case 3:
160+
const three: 3 = x;
161+
break;
162+
case true:
163+
const t: true = x;
164+
break;
165+
case false:
166+
const f: false = x;
167+
break;
168+
case "A":
169+
const a: "A" = x;
170+
break;
171+
case undefined:
172+
const undef: undefined = x;
173+
break;
174+
case null:
175+
const llun: null = x;
176+
break;
177+
case symb:
178+
const anotherSymbol: typeof symb = x;
179+
break;
180+
case symbNonUnique:
181+
const nonUniqueSymbol: symbol = x;
182+
break;
183+
}
184+
type End = isTrue<isUnknown<typeof x>>
185+
}
186+
187+
function switchTestObjects(x: unknown, y: () => void, z: { prop: number }) {
188+
switch (x) {
189+
case true:
190+
case false:
191+
const bool: boolean = x;
192+
break;
193+
case y:
194+
const obj1: object = x;
195+
break;
196+
case z:
197+
const obj2: object = x;
198+
break;
199+
}
200+
type End = isTrue<isUnknown<typeof x>>
201+
}
202+
203+
function switchResponse(x: unknown): SomeResponse {
204+
switch (x) {
205+
case 'yes':
206+
case 'no':
207+
case 'idk':
208+
return x;
209+
default:
210+
throw new Error('unknown response');
211+
}
212+
// Arguably this should be never.
213+
type End = isTrue<isUnknown<typeof x>>
214+
}
215+
216+
function switchResponseWrong(x: unknown): SomeResponse {
217+
switch (x) {
218+
case 'yes':
219+
case 'no':
220+
case 'maybe':
221+
return x; // error
222+
~~~~~~~~~
223+
!!! error TS2322: Type '"yes" | "no" | "maybe"' is not assignable to type 'SomeResponse'.
224+
!!! error TS2322: Type '"maybe"' is not assignable to type 'SomeResponse'.
225+
default:
226+
throw new Error('Can you repeat the question?');
227+
}
228+
// Arguably this should be never.
229+
type End = isTrue<isUnknown<typeof x>>
230+
}
231+

0 commit comments

Comments
 (0)
0