Skip to content

Commit 95fc667

Browse files
ahejlsbergsnovader
authored andcommitted
Fix narrowing of destructured tuples with different arities (microsoft#55744)
1 parent c1d98c5 commit 95fc667

File tree

6 files changed

+275
-14
lines changed

6 files changed

+275
-14
lines changed

src/compiler/checker.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10513,14 +10513,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1051310513
return prop ? getTypeOfSymbol(prop) : undefined;
1051410514
}
1051510515

10516-
function getTypeOfPropertyOrIndexSignature(type: Type, name: __String): Type {
10517-
return getTypeOfPropertyOfType(type, name) || getApplicableIndexInfoForName(type, name)?.type || unknownType;
10518-
}
10519-
1052010516
/**
10521-
* Similar to `getTypeOfPropertyOrIndexSignature`,
10522-
* but returns `undefined` if there is no matching property or index signature,
10523-
* and adds optionality to index signature types.
10517+
* Return the type of the matching property or index signature in the given type, or undefined
10518+
* if no matching property or index signature exists. Add optionality to index signature types.
1052410519
*/
1052510520
function getTypeOfPropertyOrIndexSignatureOfType(type: Type, name: __String): Type | undefined {
1052610521
let propType;
@@ -10681,10 +10676,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1068110676
function getTypeForBindingElement(declaration: BindingElement): Type | undefined {
1068210677
const checkMode = declaration.dotDotDotToken ? CheckMode.RestBindingElement : CheckMode.Normal;
1068310678
const parentType = getTypeForBindingElementParent(declaration.parent.parent, checkMode);
10684-
return parentType && getBindingElementTypeFromParentType(declaration, parentType);
10679+
return parentType && getBindingElementTypeFromParentType(declaration, parentType, /*noTupleBoundsCheck*/ false);
1068510680
}
1068610681

10687-
function getBindingElementTypeFromParentType(declaration: BindingElement, parentType: Type): Type {
10682+
function getBindingElementTypeFromParentType(declaration: BindingElement, parentType: Type, noTupleBoundsCheck: boolean): Type {
1068810683
// If an any type was inferred for parent, infer that for the binding element
1068910684
if (isTypeAny(parentType)) {
1069010685
return parentType;
@@ -10740,7 +10735,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1074010735
}
1074110736
else if (isArrayLikeType(parentType)) {
1074210737
const indexType = getNumberLiteralType(index);
10743-
const accessFlags = AccessFlags.ExpressionPosition | (hasDefaultValue(declaration) ? AccessFlags.NoTupleBoundsCheck : 0);
10738+
const accessFlags = AccessFlags.ExpressionPosition | (noTupleBoundsCheck || hasDefaultValue(declaration) ? AccessFlags.NoTupleBoundsCheck : 0);
1074410739
const declaredType = getIndexedAccessTypeOrUndefined(parentType, indexType, accessFlags, declaration.name) || errorType;
1074510740
type = getFlowTypeOfDestructuring(declaration, declaredType);
1074610741
}
@@ -27587,7 +27582,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2758727582
propType = removeNullable && optionalChain ? getOptionalType(propType) : propType;
2758827583
const narrowedPropType = narrowType(propType);
2758927584
return filterType(type, t => {
27590-
const discriminantType = getTypeOfPropertyOrIndexSignature(t, propName);
27585+
const discriminantType = getTypeOfPropertyOrIndexSignatureOfType(t, propName) || unknownType;
2759127586
return !(discriminantType.flags & TypeFlags.Never) && !(narrowedPropType.flags & TypeFlags.Never) && areTypesComparable(narrowedPropType, discriminantType);
2759227587
});
2759327588
}
@@ -28445,7 +28440,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2844528440
if (narrowedType.flags & TypeFlags.Never) {
2844628441
return neverType;
2844728442
}
28448-
return getBindingElementTypeFromParentType(declaration, narrowedType);
28443+
// Destructurings are validated against the parent type elsewhere. Here we disable tuple bounds
28444+
// checks because the narrowed type may have lower arity than the full parent type. For example,
28445+
// for the declaration [x, y]: [1, 2] | [3], we may have narrowed the parent type to just [3].
28446+
return getBindingElementTypeFromParentType(declaration, narrowedType, /*noTupleBoundsCheck*/ true);
2844928447
}
2845028448
}
2845128449
}
@@ -35745,7 +35743,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3574535743
function assignBindingElementTypes(pattern: BindingPattern, parentType: Type) {
3574635744
for (const element of pattern.elements) {
3574735745
if (!isOmittedExpression(element)) {
35748-
const type = getBindingElementTypeFromParentType(element, parentType);
35746+
const type = getBindingElementTypeFromParentType(element, parentType, /*noTupleBoundsCheck*/ false);
3574935747
if (element.name.kind === SyntaxKind.Identifier) {
3575035748
getSymbolLinks(getSymbolOfDeclaration(element)).type = type;
3575135749
}

tests/baselines/reference/dependentDestructuredVariables.errors.txt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
dependentDestructuredVariables.ts(334,5): error TS7022: 'value1' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
22
dependentDestructuredVariables.ts(334,5): error TS7031: Binding element 'value1' implicitly has an 'any' type.
3+
dependentDestructuredVariables.ts(431,15): error TS2322: Type 'number' is not assignable to type 'never'.
34

45

5-
==== dependentDestructuredVariables.ts (2 errors) ====
6+
==== dependentDestructuredVariables.ts (3 errors) ====
67
type Action =
78
| { kind: 'A', payload: number }
89
| { kind: 'B', payload: string };
@@ -409,4 +410,37 @@ dependentDestructuredVariables.ts(334,5): error TS7031: Binding element 'value1'
409410
const bot = new Client();
410411
bot.on("shardDisconnect", (event, shard) => console.log(`Shard ${shard} disconnected (${event.code},${event.wasClean}): ${event.reason}`));
411412
bot.on("shardDisconnect", event => console.log(`${event.code} ${event.wasClean} ${event.reason}`));
413+
414+
// Destructuring tuple types with different arities
415+
416+
function fz1([x, y]: [1, 2] | [3, 4] | [5]) {
417+
if (y === 2) {
418+
x; // 1
419+
}
420+
if (y === 4) {
421+
x; // 3
422+
}
423+
if (y === undefined) {
424+
x; // 5
425+
}
426+
if (x === 1) {
427+
y; // 2
428+
}
429+
if (x === 3) {
430+
y; // 4
431+
}
432+
if (x === 5) {
433+
y; // undefined
434+
}
435+
}
436+
437+
// Repro from #55661
438+
439+
function tooNarrow([x, y]: [1, 1] | [1, 2] | [1]) {
440+
if (y === undefined) {
441+
const shouldNotBeOk: never = x; // Error
442+
~~~~~~~~~~~~~
443+
!!! error TS2322: Type 'number' is not assignable to type 'never'.
444+
}
445+
}
412446

tests/baselines/reference/dependentDestructuredVariables.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,37 @@ declare class Client {
403403
const bot = new Client();
404404
bot.on("shardDisconnect", (event, shard) => console.log(`Shard ${shard} disconnected (${event.code},${event.wasClean}): ${event.reason}`));
405405
bot.on("shardDisconnect", event => console.log(`${event.code} ${event.wasClean} ${event.reason}`));
406+
407+
// Destructuring tuple types with different arities
408+
409+
function fz1([x, y]: [1, 2] | [3, 4] | [5]) {
410+
if (y === 2) {
411+
x; // 1
412+
}
413+
if (y === 4) {
414+
x; // 3
415+
}
416+
if (y === undefined) {
417+
x; // 5
418+
}
419+
if (x === 1) {
420+
y; // 2
421+
}
422+
if (x === 3) {
423+
y; // 4
424+
}
425+
if (x === 5) {
426+
y; // undefined
427+
}
428+
}
429+
430+
// Repro from #55661
431+
432+
function tooNarrow([x, y]: [1, 1] | [1, 2] | [1]) {
433+
if (y === undefined) {
434+
const shouldNotBeOk: never = x; // Error
435+
}
436+
}
406437

407438

408439
//// [dependentDestructuredVariables.js]
@@ -707,6 +738,33 @@ const fa3 = (guard, value) => {
707738
const bot = new Client();
708739
bot.on("shardDisconnect", (event, shard) => console.log(`Shard ${shard} disconnected (${event.code},${event.wasClean}): ${event.reason}`));
709740
bot.on("shardDisconnect", event => console.log(`${event.code} ${event.wasClean} ${event.reason}`));
741+
// Destructuring tuple types with different arities
742+
function fz1([x, y]) {
743+
if (y === 2) {
744+
x; // 1
745+
}
746+
if (y === 4) {
747+
x; // 3
748+
}
749+
if (y === undefined) {
750+
x; // 5
751+
}
752+
if (x === 1) {
753+
y; // 2
754+
}
755+
if (x === 3) {
756+
y; // 4
757+
}
758+
if (x === 5) {
759+
y; // undefined
760+
}
761+
}
762+
// Repro from #55661
763+
function tooNarrow([x, y]) {
764+
if (y === undefined) {
765+
const shouldNotBeOk = x; // Error
766+
}
767+
}
710768

711769

712770
//// [dependentDestructuredVariables.d.ts]
@@ -855,3 +913,5 @@ declare class Client {
855913
on<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): void;
856914
}
857915
declare const bot: Client;
916+
declare function fz1([x, y]: [1, 2] | [3, 4] | [5]): void;
917+
declare function tooNarrow([x, y]: [1, 1] | [1, 2] | [1]): void;

tests/baselines/reference/dependentDestructuredVariables.symbols

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,3 +1037,66 @@ bot.on("shardDisconnect", event => console.log(`${event.code} ${event.wasClean}
10371037
>event : Symbol(event, Decl(dependentDestructuredVariables.ts, 401, 25))
10381038
>reason : Symbol(CloseEvent.reason, Decl(lib.dom.d.ts, --, --))
10391039

1040+
// Destructuring tuple types with different arities
1041+
1042+
function fz1([x, y]: [1, 2] | [3, 4] | [5]) {
1043+
>fz1 : Symbol(fz1, Decl(dependentDestructuredVariables.ts, 401, 99))
1044+
>x : Symbol(x, Decl(dependentDestructuredVariables.ts, 405, 14))
1045+
>y : Symbol(y, Decl(dependentDestructuredVariables.ts, 405, 16))
1046+
1047+
if (y === 2) {
1048+
>y : Symbol(y, Decl(dependentDestructuredVariables.ts, 405, 16))
1049+
1050+
x; // 1
1051+
>x : Symbol(x, Decl(dependentDestructuredVariables.ts, 405, 14))
1052+
}
1053+
if (y === 4) {
1054+
>y : Symbol(y, Decl(dependentDestructuredVariables.ts, 405, 16))
1055+
1056+
x; // 3
1057+
>x : Symbol(x, Decl(dependentDestructuredVariables.ts, 405, 14))
1058+
}
1059+
if (y === undefined) {
1060+
>y : Symbol(y, Decl(dependentDestructuredVariables.ts, 405, 16))
1061+
>undefined : Symbol(undefined)
1062+
1063+
x; // 5
1064+
>x : Symbol(x, Decl(dependentDestructuredVariables.ts, 405, 14))
1065+
}
1066+
if (x === 1) {
1067+
>x : Symbol(x, Decl(dependentDestructuredVariables.ts, 405, 14))
1068+
1069+
y; // 2
1070+
>y : Symbol(y, Decl(dependentDestructuredVariables.ts, 405, 16))
1071+
}
1072+
if (x === 3) {
1073+
>x : Symbol(x, Decl(dependentDestructuredVariables.ts, 405, 14))
1074+
1075+
y; // 4
1076+
>y : Symbol(y, Decl(dependentDestructuredVariables.ts, 405, 16))
1077+
}
1078+
if (x === 5) {
1079+
>x : Symbol(x, Decl(dependentDestructuredVariables.ts, 405, 14))
1080+
1081+
y; // undefined
1082+
>y : Symbol(y, Decl(dependentDestructuredVariables.ts, 405, 16))
1083+
}
1084+
}
1085+
1086+
// Repro from #55661
1087+
1088+
function tooNarrow([x, y]: [1, 1] | [1, 2] | [1]) {
1089+
>tooNarrow : Symbol(tooNarrow, Decl(dependentDestructuredVariables.ts, 424, 1))
1090+
>x : Symbol(x, Decl(dependentDestructuredVariables.ts, 428, 20))
1091+
>y : Symbol(y, Decl(dependentDestructuredVariables.ts, 428, 22))
1092+
1093+
if (y === undefined) {
1094+
>y : Symbol(y, Decl(dependentDestructuredVariables.ts, 428, 22))
1095+
>undefined : Symbol(undefined)
1096+
1097+
const shouldNotBeOk: never = x; // Error
1098+
>shouldNotBeOk : Symbol(shouldNotBeOk, Decl(dependentDestructuredVariables.ts, 430, 13))
1099+
>x : Symbol(x, Decl(dependentDestructuredVariables.ts, 428, 20))
1100+
}
1101+
}
1102+

tests/baselines/reference/dependentDestructuredVariables.types

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,3 +1188,78 @@ bot.on("shardDisconnect", event => console.log(`${event.code} ${event.wasClean}
11881188
>event : CloseEvent
11891189
>reason : string
11901190

1191+
// Destructuring tuple types with different arities
1192+
1193+
function fz1([x, y]: [1, 2] | [3, 4] | [5]) {
1194+
>fz1 : ([x, y]: [1, 2] | [3, 4] | [5]) => void
1195+
>x : 1 | 3 | 5
1196+
>y : 2 | 4 | undefined
1197+
1198+
if (y === 2) {
1199+
>y === 2 : boolean
1200+
>y : 2 | 4 | undefined
1201+
>2 : 2
1202+
1203+
x; // 1
1204+
>x : 1
1205+
}
1206+
if (y === 4) {
1207+
>y === 4 : boolean
1208+
>y : 2 | 4 | undefined
1209+
>4 : 4
1210+
1211+
x; // 3
1212+
>x : 3
1213+
}
1214+
if (y === undefined) {
1215+
>y === undefined : boolean
1216+
>y : 2 | 4 | undefined
1217+
>undefined : undefined
1218+
1219+
x; // 5
1220+
>x : 5
1221+
}
1222+
if (x === 1) {
1223+
>x === 1 : boolean
1224+
>x : 1 | 3 | 5
1225+
>1 : 1
1226+
1227+
y; // 2
1228+
>y : 2
1229+
}
1230+
if (x === 3) {
1231+
>x === 3 : boolean
1232+
>x : 1 | 3 | 5
1233+
>3 : 3
1234+
1235+
y; // 4
1236+
>y : 4
1237+
}
1238+
if (x === 5) {
1239+
>x === 5 : boolean
1240+
>x : 1 | 3 | 5
1241+
>5 : 5
1242+
1243+
y; // undefined
1244+
>y : undefined
1245+
}
1246+
}
1247+
1248+
// Repro from #55661
1249+
1250+
function tooNarrow([x, y]: [1, 1] | [1, 2] | [1]) {
1251+
>tooNarrow : ([x, y]: [1, 1] | [1, 2] | [1]) => void
1252+
>x : 1
1253+
>y : 2 | 1 | undefined
1254+
1255+
if (y === undefined) {
1256+
>y === undefined : boolean
1257+
>y : 2 | 1 | undefined
1258+
>undefined : undefined
1259+
1260+
const shouldNotBeOk: never = x; // Error
1261+
>shouldNotBeOk : never
1262+
>x : 1
1263+
}
1264+
}
1265+

tests/cases/conformance/controlFlow/dependentDestructuredVariables.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,34 @@ declare class Client {
405405
const bot = new Client();
406406
bot.on("shardDisconnect", (event, shard) => console.log(`Shard ${shard} disconnected (${event.code},${event.wasClean}): ${event.reason}`));
407407
bot.on("shardDisconnect", event => console.log(`${event.code} ${event.wasClean} ${event.reason}`));
408+
409+
// Destructuring tuple types with different arities
410+
411+
function fz1([x, y]: [1, 2] | [3, 4] | [5]) {
412+
if (y === 2) {
413+
x; // 1
414+
}
415+
if (y === 4) {
416+
x; // 3
417+
}
418+
if (y === undefined) {
419+
x; // 5
420+
}
421+
if (x === 1) {
422+
y; // 2
423+
}
424+
if (x === 3) {
425+
y; // 4
426+
}
427+
if (x === 5) {
428+
y; // undefined
429+
}
430+
}
431+
432+
// Repro from #55661
433+
434+
function tooNarrow([x, y]: [1, 1] | [1, 2] | [1]) {
435+
if (y === undefined) {
436+
const shouldNotBeOk: never = x; // Error
437+
}
438+
}

0 commit comments

Comments
 (0)