Skip to content

Using variable defined by array destructuring assignment in default value of variables defined afterwards. #49989

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

Closed
farazsa88 opened this issue Jul 21, 2022 · 4 comments · Fixed by #56753 or #59183
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue Help Wanted You can do this
Milestone

Comments

@farazsa88
Copy link

Bug Report

🔎 Search Terms

Array destructuring assignment default value 7022

🕗 Version & Regression Information

Exists in 3.3.3 - 4.8.0-beta

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about this behaviour

⏯ Playground Link

Playground link with relevant code

💻 Code

const [a, b = a + 1] = [2];
console.log(`a: ${a}, b: ${b}`);

🙁 Actual behavior

Types of a and b inferred as any, or if noImplicitAny enabled, shows error: 'a' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.(7022)

🙂 Expected behavior

Types of both a and b should be inferred as number.

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript Help Wanted You can do this labels Jul 28, 2022
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Jul 28, 2022
babakks added a commit to babakks/TypeScript that referenced this issue Aug 8, 2022
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>

diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index abf3e9d..21eefd8 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -9440,16 +9440,32 @@ namespace ts {
         // Return the type implied by a binding pattern element. This is the type of the initializer of the element if
         // one is present. Otherwise, if the element is itself a binding pattern, it is the type implied by the binding
         // pattern. Otherwise, it is the type any.
-        function getTypeFromBindingElement(element: BindingElement, includePatternInType?: boolean, reportErrors?: boolean): Type {
+        function getTypeFromBindingElement(element: BindingElement, includePatternInType?: boolean, reportErrors?: boolean, skipNonLiteralInitializer?: boolean): Type {
             if (element.initializer) {
                 // The type implied by a binding pattern is independent of context, so we check the initializer with no
                 // contextual type or, if the element itself is a binding pattern, with the type implied by that binding
                 // pattern.
-                const contextualType = isBindingPattern(element.name) ? getTypeFromBindingPattern(element.name, /*includePatternInType*/ true, /*reportErrors*/ false) : unknownType;
-                return addOptionality(widenTypeInferredFromInitializer(element, checkDeclarationInitializer(element, CheckMode.Normal, contextualType)));
+                const contextualType = isBindingPattern(element.name)
+                    ? getTypeFromBindingPattern(element.name, /*includePatternInType*/ true, /*reportErrors*/ false, skipNonLiteralInitializer)
+                    : unknownType;
+
+                // (microsoft#49989)
+                // If `skipNonLiteralInitializer` is not set, in cases where the intitializer is an identifier pointing
+                // to a sibling symbol in the same declaration, a false circular relationship will be concluded. For
+                // example, take the declarations below:
+                //
+                //   const [a, b = a] = [1];
+                //   const {a, b = a} = {a: 1};
+                //
+                // Here when the `element` is the second binding element, `b = a`, the initializer is `a` which itself
+                // is defined within the same binding pattern.
+                const type = skipNonLiteralInitializer && !isLiteralExpression(element.initializer)
+                    ? contextualType
+                    : checkDeclarationInitializer(element, CheckMode.Normal, contextualType);
+                return addOptionality(widenTypeInferredFromInitializer(element, type));
             }
             if (isBindingPattern(element.name)) {
-                return getTypeFromBindingPattern(element.name, includePatternInType, reportErrors);
+                return getTypeFromBindingPattern(element.name, includePatternInType, reportErrors, skipNonLiteralInitializer);
             }
             if (reportErrors && !declarationBelongsToPrivateAmbientMember(element)) {
                 reportImplicitAny(element, anyType);
@@ -9458,7 +9474,7 @@ namespace ts {
         }

         // Return the type implied by an object binding pattern
-        function getTypeFromObjectBindingPattern(pattern: ObjectBindingPattern, includePatternInType: boolean, reportErrors: boolean): Type {
+        function getTypeFromObjectBindingPattern(pattern: ObjectBindingPattern, includePatternInType: boolean, reportErrors: boolean, skipNonLiteralInitializer: boolean): Type {
             const members = createSymbolTable();
             let stringIndexInfo: IndexInfo | undefined;
             let objectFlags = ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral;
@@ -9478,7 +9494,7 @@ namespace ts {
                 const text = getPropertyNameFromType(exprType);
                 const flags = SymbolFlags.Property | (e.initializer ? SymbolFlags.Optional : 0);
                 const symbol = createSymbol(flags, text);
-                symbol.type = getTypeFromBindingElement(e, includePatternInType, reportErrors);
+                symbol.type = getTypeFromBindingElement(e, includePatternInType, reportErrors, skipNonLiteralInitializer);
                 symbol.bindingElement = e;
                 members.set(symbol.escapedName, symbol);
             });
@@ -9492,14 +9508,14 @@ namespace ts {
         }

         // Return the type implied by an array binding pattern
-        function getTypeFromArrayBindingPattern(pattern: BindingPattern, includePatternInType: boolean, reportErrors: boolean): Type {
+        function getTypeFromArrayBindingPattern(pattern: BindingPattern, includePatternInType: boolean, reportErrors: boolean, skipNonLiteralInitializer: boolean): Type {
             const elements = pattern.elements;
             const lastElement = lastOrUndefined(elements);
             const restElement = lastElement && lastElement.kind === SyntaxKind.BindingElement && lastElement.dotDotDotToken ? lastElement : undefined;
             if (elements.length === 0 || elements.length === 1 && restElement) {
                 return languageVersion >= ScriptTarget.ES2015 ? createIterableType(anyType) : anyArrayType;
             }
-            const elementTypes = map(elements, e => isOmittedExpression(e) ? anyType : getTypeFromBindingElement(e, includePatternInType, reportErrors));
+            const elementTypes = map(elements, e => isOmittedExpression(e) ? anyType : getTypeFromBindingElement(e, includePatternInType, reportErrors, skipNonLiteralInitializer));
             const minLength = findLastIndex(elements, e => !(e === restElement || isOmittedExpression(e) || hasDefaultValue(e)), elements.length - 1) + 1;
             const elementFlags = map(elements, (e, i) => e === restElement ? ElementFlags.Rest : i >= minLength ? ElementFlags.Optional : ElementFlags.Required);
             let result = createTupleType(elementTypes, elementFlags) as TypeReference;
@@ -9518,10 +9534,10 @@ namespace ts {
         // used as the contextual type of an initializer associated with the binding pattern. Also, for a destructuring
         // parameter with no type annotation or initializer, the type implied by the binding pattern becomes the type of
         // the parameter.
-        function getTypeFromBindingPattern(pattern: BindingPattern, includePatternInType = false, reportErrors = false): Type {
+        function getTypeFromBindingPattern(pattern: BindingPattern, includePatternInType = false, reportErrors = false, skipNonLiteralInitializer = false): Type {
             return pattern.kind === SyntaxKind.ObjectBindingPattern
-                ? getTypeFromObjectBindingPattern(pattern, includePatternInType, reportErrors)
-                : getTypeFromArrayBindingPattern(pattern, includePatternInType, reportErrors);
+                ? getTypeFromObjectBindingPattern(pattern, includePatternInType, reportErrors, skipNonLiteralInitializer)
+                : getTypeFromArrayBindingPattern(pattern, includePatternInType, reportErrors, skipNonLiteralInitializer);
         }

         // Return the type associated with a variable, parameter, or property declaration. In the simple case this is the type
@@ -26753,7 +26769,7 @@ namespace ts {
                     return result;
                 }
                 if (!(contextFlags! & ContextFlags.SkipBindingPatterns) && isBindingPattern(declaration.name) && declaration.name.elements.length > 0) {
-                    return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ true, /*reportErrors*/ false);
+                    return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ true, /*reportErrors*/ false, /*skipNonLiteralInitializer*/ true);
                 }
             }
             return undefined;
babakks added a commit to babakks/TypeScript that referenced this issue Aug 9, 2022
… sibling definitions

Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>

diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index abf3e9d..793ffb3 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -9445,8 +9445,33 @@ namespace ts {
                 // The type implied by a binding pattern is independent of context, so we check the initializer with no
                 // contextual type or, if the element itself is a binding pattern, with the type implied by that binding
                 // pattern.
-                const contextualType = isBindingPattern(element.name) ? getTypeFromBindingPattern(element.name, /*includePatternInType*/ true, /*reportErrors*/ false) : unknownType;
-                return addOptionality(widenTypeInferredFromInitializer(element, checkDeclarationInitializer(element, CheckMode.Normal, contextualType)));
+                if (isBindingPattern(element.name)) {
+                    const contextualType = getTypeFromBindingPattern(element.name, /*includePatternInType*/ true, /*reportErrors*/ false);
+                    return addOptionality(widenTypeInferredFromInitializer(element, checkDeclarationInitializer(element, CheckMode.Normal, contextualType)));
+                }
+
+                // (microsoft#49989)
+                // In cases where the intitializer is an identifier referencing a sibling symbol (i.e., one that is
+                // defined in the same declaration) a false circular relationship will be concluded. For example, take
+                // the declarations below:
+                //
+                //   const [a, b = a] = [1];
+                //   const {a, b = a} = {a: 1};
+                //
+                // Here, when the `element` is the second binding element (i.e., `b = a`) the initializer is `a` which
+                // itself is defined within the same binding pattern.
+                //
+                // So, we check the initializer expression for any references to sibling symbols and if any, then we'd
+                // conclude the binding element type as `unknownType` and thus skip further circulations in type
+                // checking.
+                const siblings = mapDefined(element.parent.elements, x => x !== element && isBindingElement(x) && isIdentifier(x.name) ? x : undefined);
+                const checkIsSiblingInvolved = (node: Node) => {
+                    const declaration = isIdentifier(node) && getReferencedValueDeclaration(node);
+                    return declaration && isBindingElement(declaration) && siblings.includes(declaration);
+                };
+                const isSiblingElementInvolded = checkIsSiblingInvolved(element.initializer) || forEachChildRecursively(element.initializer, checkIsSiblingInvolved);
+                return isSiblingElementInvolded ? anyType
+                    : addOptionality(widenTypeInferredFromInitializer(element, checkDeclarationInitializer(element, CheckMode.Normal, unknownType)));
             }
             if (isBindingPattern(element.name)) {
                 return getTypeFromBindingPattern(element.name, includePatternInType, reportErrors);
@babakks
Copy link
Contributor

babakks commented Aug 9, 2022

@RyanCavanaugh I've submitted a PR for this issue. Would you please check it later?

@jcalz
Copy link
Contributor

jcalz commented Dec 12, 2023

Not sure if the issue reported this SO question with the code:

declare const o: { a?: number, b?: number } | undefined
const { a, b = a } = o ?? {}; // error!
// ---> ~  'a' 
// implicitly has type 'any' because it does not have a type annotation and is 
// referenced directly or indirectly in its own initializer.(7022)

Playground link

is the same issue or not. Waiting to see if any fix for this addresses it; if not, I might open a new issue for it (unless someone can point to a more appropriate existing issue)

@babakks
Copy link
Contributor

babakks commented Jan 11, 2024

@jcalz I think the issue you mentioned is solved by the PR. You can check it via this custom-build playground. As I checked the type inference is now showing number | undefined for a and b symbols.

@jcalz
Copy link
Contributor

jcalz commented Jan 11, 2024

Indeed, looks like it. And since that got merged it's also visible in the nightly build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment