From a97e52e81083f277a66f3f63064c9ff31d69a770 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sat, 25 Oct 2025 22:19:00 +0200 Subject: [PATCH 01/35] feat(eslint-plugin): [no-useless-default-assignment] add rule --- .../rules/no-useless-default-assignment.mdx | 88 ++++++ .../eslint-plugin/src/configs/eslintrc/all.ts | 1 + .../configs/eslintrc/disable-type-checked.ts | 1 + .../eslint-plugin/src/configs/flat/all.ts | 1 + .../src/configs/flat/disable-type-checked.ts | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../rules/no-useless-default-assignment.ts | 257 ++++++++++++++++++ .../no-useless-default-assignment.shot | 46 ++++ packages/eslint-plugin/tests/docs.test.mts | 1 + .../no-useless-default-assignment.test.ts | 245 +++++++++++++++++ .../no-useless-default-assignment.shot | 25 ++ 11 files changed, 668 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx create mode 100644 packages/eslint-plugin/src/rules/no-useless-default-assignment.ts create mode 100644 packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-default-assignment.shot create mode 100644 packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts create mode 100644 packages/eslint-plugin/tests/schema-snapshots/no-useless-default-assignment.shot diff --git a/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx new file mode 100644 index 000000000000..811bf8c4f1c4 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx @@ -0,0 +1,88 @@ +--- +description: 'Disallow default values that will never be used.' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-useless-default-assignment** for documentation. + +[Default parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters) and [default values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#default_value) are only used if the parameter or property is `undefined` (either because its value is `undefined` or because it is missing). +If the type of a parameter or property can never be `undefined`, then the default is never actually used. + +## Examples + + + + +```ts +function Bar({ foo = '' }: { foo: string }) { + return foo; +} + +const { foo = '' } = { foo: 'bar' }; + +[1, 2, 3].map((a = 42) => a + 1); + +function test(a: string = 'default') { + return a; +} + +const test = (a: string = 'default') => a; +``` + + + + +```ts +function Bar({ foo = '' }: { foo?: string }) { + return foo; +} + +[1, 2, 3, undefined].map((a = 42) => a + 1); + +const obj: { a?: string } = {}; +const { a = 'default' } = obj; + +const obj: { a: string | undefined } = { a: undefined }; +const { a = 'default' } = obj; + +function test(a: string | undefined = 'default') { + return a; +} + +function test(a: any = 'default') { + return a; +} + +function test(a: unknown = 'default') { + return a; +} +``` + + + + +## Options + +### `allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing` + +{/* insert option description */} + +:::danger Deprecated +This option will be removed in the next major version of typescript-eslint. +::: + +If this is set to `false`, then the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`. + +Without `strictNullChecks`, TypeScript essentially erases `undefined` and `null` from the types. This means when this rule inspects the types from a variable, **it will not be able to tell that the variable might be `null` or `undefined`**, which essentially makes this rule useless. + +You should be using `strictNullChecks` to ensure complete type-safety in your codebase. + +If for some reason you cannot turn on `strictNullChecks`, but still want to use this rule - you can use this option to allow it - but know that the behavior of this rule is _undefined_ with the compiler option turned off. We will not accept bug reports if you are using this option. + +## When Not To Use It + +If your project is not accurately typed or `strictNullChecks` is disabled. diff --git a/packages/eslint-plugin/src/configs/eslintrc/all.ts b/packages/eslint-plugin/src/configs/eslintrc/all.ts index 34f7fd540450..e00dcfeb6ed4 100644 --- a/packages/eslint-plugin/src/configs/eslintrc/all.ts +++ b/packages/eslint-plugin/src/configs/eslintrc/all.ts @@ -118,6 +118,7 @@ export = { '@typescript-eslint/no-use-before-define': 'error', 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/no-useless-default-assignment': 'error', '@typescript-eslint/no-useless-empty-export': 'error', '@typescript-eslint/no-wrapper-object-types': 'error', '@typescript-eslint/non-nullable-type-assertion-style': 'error', diff --git a/packages/eslint-plugin/src/configs/eslintrc/disable-type-checked.ts b/packages/eslint-plugin/src/configs/eslintrc/disable-type-checked.ts index 6853a151722d..277f99228c1d 100644 --- a/packages/eslint-plugin/src/configs/eslintrc/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/eslintrc/disable-type-checked.ts @@ -44,6 +44,7 @@ export = { '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/no-unsafe-type-assertion': 'off', '@typescript-eslint/no-unsafe-unary-minus': 'off', + '@typescript-eslint/no-useless-default-assignment': 'off', '@typescript-eslint/non-nullable-type-assertion-style': 'off', '@typescript-eslint/only-throw-error': 'off', '@typescript-eslint/prefer-destructuring': 'off', diff --git a/packages/eslint-plugin/src/configs/flat/all.ts b/packages/eslint-plugin/src/configs/flat/all.ts index 777028d8580c..308aeb5d5072 100644 --- a/packages/eslint-plugin/src/configs/flat/all.ts +++ b/packages/eslint-plugin/src/configs/flat/all.ts @@ -132,6 +132,7 @@ export default ( '@typescript-eslint/no-use-before-define': 'error', 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/no-useless-default-assignment': 'error', '@typescript-eslint/no-useless-empty-export': 'error', '@typescript-eslint/no-wrapper-object-types': 'error', '@typescript-eslint/non-nullable-type-assertion-style': 'error', diff --git a/packages/eslint-plugin/src/configs/flat/disable-type-checked.ts b/packages/eslint-plugin/src/configs/flat/disable-type-checked.ts index 5a48e1722775..7603dde91c7a 100644 --- a/packages/eslint-plugin/src/configs/flat/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/flat/disable-type-checked.ts @@ -51,6 +51,7 @@ export default ( '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/no-unsafe-type-assertion': 'off', '@typescript-eslint/no-unsafe-unary-minus': 'off', + '@typescript-eslint/no-useless-default-assignment': 'off', '@typescript-eslint/non-nullable-type-assertion-style': 'off', '@typescript-eslint/only-throw-error': 'off', '@typescript-eslint/prefer-destructuring': 'off', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 2a82c7e18c6b..b4f4266a9850 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -91,6 +91,7 @@ import noUnusedExpressions from './no-unused-expressions'; import noUnusedVars from './no-unused-vars'; import noUseBeforeDefine from './no-use-before-define'; import noUselessConstructor from './no-useless-constructor'; +import noUselessDefaultAssignment from './no-useless-default-assignment'; import noUselessEmptyExport from './no-useless-empty-export'; import noVarRequires from './no-var-requires'; import noWrapperObjectTypes from './no-wrapper-object-types'; @@ -225,6 +226,7 @@ const rules = { 'no-unused-vars': noUnusedVars, 'no-use-before-define': noUseBeforeDefine, 'no-useless-constructor': noUselessConstructor, + 'no-useless-default-assignment': noUselessDefaultAssignment, 'no-useless-empty-export': noUselessEmptyExport, 'no-var-requires': noVarRequires, 'no-wrapper-object-types': noWrapperObjectTypes, diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts new file mode 100644 index 000000000000..ac35e536184c --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -0,0 +1,257 @@ +import type { TSESTree } from '@typescript-eslint/utils'; + +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import * as tsutils from 'ts-api-utils'; +import * as ts from 'typescript'; + +import { + createRule, + getParserServices, + isTypeAnyType, + isTypeFlagSet, + isTypeUnknownType, +} from '../util'; + +type Options = [ + { + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; + }, +]; + +type MessageId = + | 'noStrictNullCheck' + | 'suggestRemoveDefault' + | 'uselessDefaultAssignment'; + +export default createRule({ + name: 'no-useless-default-assignment', + meta: { + type: 'suggestion', + docs: { + description: 'Disallow default values that will never be used', + requiresTypeChecking: true, + }, + hasSuggestions: true, + messages: { + noStrictNullCheck: + 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', + suggestRemoveDefault: 'Remove useless default value', + uselessDefaultAssignment: + 'Default value is useless because the {{ type }} is not optional.', + }, + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: { + type: 'boolean', + description: + 'Unless this is set to `true`, the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`.', + }, + }, + }, + ], + }, + defaultOptions: [ + { + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, + }, + ], + create(context, [options]) { + const services = getParserServices(context); + const checker = services.program.getTypeChecker(); + const compilerOptions = services.program.getCompilerOptions(); + + const isStrictNullChecks = tsutils.isStrictCompilerOptionEnabled( + compilerOptions, + 'strictNullChecks', + ); + + if ( + !isStrictNullChecks && + options.allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true + ) { + context.report({ + loc: { + start: { column: 0, line: 0 }, + end: { column: 0, line: 0 }, + }, + messageId: 'noStrictNullCheck', + }); + } + + function canBeUndefined(type: ts.Type): boolean { + // any and unknown can be undefined + if (isTypeAnyType(type) || isTypeUnknownType(type)) { + return true; + } + // Check if any part of the union includes undefined + return tsutils + .unionConstituents(type) + .some(part => isTypeFlagSet(part, ts.TypeFlags.Undefined)); + } + + function getPropertyType( + objectType: ts.Type, + propertyName: string, + ): ts.Type | null { + const symbol = objectType.getProperty(propertyName); + if (!symbol) { + return null; + } + return checker.getTypeOfSymbol(symbol); + } + + function checkAssignmentPattern(node: TSESTree.AssignmentPattern): void { + const parent = node.parent; + + // Handle direct function parameters (not destructured) + if ( + parent.type === AST_NODE_TYPES.FunctionExpression || + parent.type === AST_NODE_TYPES.ArrowFunctionExpression || + parent.type === AST_NODE_TYPES.FunctionDeclaration + ) { + const tsNode = services.esTreeNodeToTSNodeMap.get(node.left); + const type = checker.getTypeAtLocation(tsNode); + if (!canBeUndefined(type)) { + reportUselessDefault(node, 'parameter'); + } + return; + } + + // Handle destructuring patterns + if (parent.type === AST_NODE_TYPES.Property) { + // This is a property in an object destructuring pattern + const objectPattern = parent.parent as TSESTree.ObjectPattern | null; + if (!objectPattern) { + return; + } + + // Get the source type being destructured + const sourceType = getSourceTypeForPattern(objectPattern); + if (!sourceType) { + return; + } + + // Get the property name + const propertyName = getPropertyName(parent.key); + if (!propertyName) { + return; + } + + // Get the type of this specific property + const propertyType = getPropertyType(sourceType, propertyName); + if (!propertyType) { + return; + } + + if (!canBeUndefined(propertyType)) { + reportUselessDefault(node, 'property'); + } + } + } + + function getSourceTypeForPattern( + pattern: TSESTree.ArrayPattern | TSESTree.ObjectPattern, + ): ts.Type | null { + let currentNode: TSESTree.Node = pattern; + + // Walk up through nested patterns + while ( + currentNode.parent.type === AST_NODE_TYPES.AssignmentPattern || + currentNode.parent.type === AST_NODE_TYPES.Property || + currentNode.parent.type === AST_NODE_TYPES.ObjectPattern || + currentNode.parent.type === AST_NODE_TYPES.ArrayPattern + ) { + currentNode = currentNode.parent; + } + + const parent = currentNode.parent; + + // Handle variable declarator + if (parent.type === AST_NODE_TYPES.VariableDeclarator && parent.init) { + const tsNode = services.esTreeNodeToTSNodeMap.get(parent.init); + return checker.getTypeAtLocation(tsNode); + } + + // Handle function parameter + if ( + parent.type === AST_NODE_TYPES.FunctionExpression || + parent.type === AST_NODE_TYPES.ArrowFunctionExpression || + parent.type === AST_NODE_TYPES.FunctionDeclaration + ) { + const paramIndex = parent.params.indexOf( + currentNode as TSESTree.Parameter, + ); + if (paramIndex === -1) { + return null; + } + + const tsFunc = services.esTreeNodeToTSNodeMap.get(parent); + if (!ts.isFunctionLike(tsFunc)) { + return null; + } + + const signature = checker.getSignatureFromDeclaration(tsFunc); + if (!signature) { + return null; + } + + const params = signature.getParameters(); + if (paramIndex >= params.length) { + return null; + } + + return checker.getTypeOfSymbol(params[paramIndex]); + } + + return null; + } + + function getPropertyName( + key: TSESTree.Expression | TSESTree.PrivateIdentifier, + ): string | null { + switch (key.type) { + case AST_NODE_TYPES.Identifier: + return key.name; + case AST_NODE_TYPES.Literal: + return String(key.value); + default: + return null; + } + } + + function reportUselessDefault( + node: TSESTree.AssignmentPattern, + type: 'parameter' | 'property', + ): void { + context.report({ + node: node.right, + messageId: 'uselessDefaultAssignment', + data: { type }, + suggest: [ + { + messageId: 'suggestRemoveDefault', + fix(fixer) { + // Remove the ` = default_value` part + const leftNode = node.left; + const tokenBefore = context.sourceCode.getTokenBefore(node.right); + if (!tokenBefore?.value || tokenBefore.value !== '=') { + return null; + } + // Remove from before the = to the end of the default value + // Find the start position (including whitespace before =) + const leftEnd = leftNode.range[1]; + return fixer.removeRange([leftEnd, node.range[1]]); + }, + }, + ], + }); + } + + return { + AssignmentPattern: checkAssignmentPattern, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-default-assignment.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-default-assignment.shot new file mode 100644 index 000000000000..b89f97453a6f --- /dev/null +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-default-assignment.shot @@ -0,0 +1,46 @@ +Incorrect + +function Bar({ foo = '' }: { foo: string }) { + ~~ Default value is useless because the property is not optional. + return foo; +} + +const { foo = '' } = { foo: 'bar' }; + ~~ Default value is useless because the property is not optional. + +[1, 2, 3].map((a = 42) => a + 1); + ~~ Default value is useless because the parameter is not optional. + +function test(a: string = 'default') { + ~~~~~~~~~ Default value is useless because the parameter is not optional. + return a; +} + +const test = (a: string = 'default') => a; + ~~~~~~~~~ Default value is useless because the parameter is not optional. + +Correct + +function Bar({ foo = '' }: { foo?: string }) { + return foo; +} + +[1, 2, 3, undefined].map((a = 42) => a + 1); + +const obj: { a?: string } = {}; +const { a = 'default' } = obj; + +const obj: { a: string | undefined } = { a: undefined }; +const { a = 'default' } = obj; + +function test(a: string | undefined = 'default') { + return a; +} + +function test(a: any = 'default') { + return a; +} + +function test(a: unknown = 'default') { + return a; +} diff --git a/packages/eslint-plugin/tests/docs.test.mts b/packages/eslint-plugin/tests/docs.test.mts index ea12c3e02179..3f3725e1ff9f 100644 --- a/packages/eslint-plugin/tests/docs.test.mts +++ b/packages/eslint-plugin/tests/docs.test.mts @@ -196,6 +196,7 @@ describe('Validating rule docs', () => { 'switch-exhaustiveness-check', 'unbound-method', 'no-unnecessary-boolean-literal-compare', + 'no-useless-default-assignment', ]); it('All rules must have a corresponding rule doc', async () => { diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts new file mode 100644 index 000000000000..73ded6c716a2 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -0,0 +1,245 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-useless-default-assignment'; +import { getFixturesRootDir } from '../RuleTester'; + +const rootPath = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: rootPath, + }, + }, +}); + +ruleTester.run('no-useless-default-assignment', rule, { + valid: [ + // React props destructuring with optional property + ` + function Bar({ foo = '' }: { foo?: string }) { + return foo; + } + `, + // Destructuring assignment - no default needed when property exists + ` + const { foo } = { foo: 'bar' }; + `, + // Default parameter with undefined in union + ` + [1, 2, 3, undefined].map((a = 42) => a + 1); + `, + // Default parameter with optional parameter type + ` + function test(a?: number) { + return a; + } + `, + // Object destructuring with optional property + ` + const obj: { a?: string } = {}; + const { a = 'default' } = obj; + `, + // Function parameter with union including undefined + ` + function test(a: string | undefined = 'default') { + return a; + } + `, + // Destructuring with union type including undefined + ` + const obj: { a: string | undefined } = { a: undefined }; + const { a = 'default' } = obj; + `, + // Array parameter that can be undefined + ` + function test(arr: number[] | undefined = []) { + return arr; + } + `, + // Complex destructuring with optional + ` + function Bar({ nested: { foo = '' } = {} }: { nested?: { foo?: string } }) { + return foo; + } + `, + // Default with any type (should not error) + ` + function test(a: any = 'default') { + return a; + } + `, + // Default with unknown type (should not error) + ` + function test(a: unknown = 'default') { + return a; + } + `, + ], + invalid: [ + // React props destructuring - property is not optional + { + code: ` + function Bar({ foo = '' }: { foo: string }) { + return foo; + } + `, + errors: [ + { + data: { type: 'property' }, + line: 2, + messageId: 'uselessDefaultAssignment', + suggestions: [ + { + messageId: 'suggestRemoveDefault', + output: ` + function Bar({ foo }: { foo: string }) { + return foo; + } + `, + }, + ], + }, + ], + }, + // Destructuring assignment - property is not optional + { + code: ` + const { foo = '' } = { foo: 'bar' }; + `, + errors: [ + { + data: { type: 'property' }, + line: 2, + messageId: 'uselessDefaultAssignment', + suggestions: [ + { + messageId: 'suggestRemoveDefault', + output: ` + const { foo } = { foo: 'bar' }; + `, + }, + ], + }, + ], + }, + // Default parameter - array elements are never undefined + { + code: ` + [1, 2, 3].map((a = 42) => a + 1); + `, + errors: [ + { + data: { type: 'parameter' }, + line: 2, + messageId: 'uselessDefaultAssignment', + suggestions: [ + { + messageId: 'suggestRemoveDefault', + output: ` + [1, 2, 3].map((a) => a + 1); + `, + }, + ], + }, + ], + }, + // Function parameter that's not optional + { + code: ` + function test(a: string = 'default') { + return a; + } + `, + errors: [ + { + data: { type: 'parameter' }, + line: 2, + messageId: 'uselessDefaultAssignment', + suggestions: [ + { + messageId: 'suggestRemoveDefault', + output: ` + function test(a: string) { + return a; + } + `, + }, + ], + }, + ], + }, + // Arrow function parameter + { + code: ` + const test = (a: string = 'default') => a; + `, + errors: [ + { + data: { type: 'parameter' }, + line: 2, + messageId: 'uselessDefaultAssignment', + suggestions: [ + { + messageId: 'suggestRemoveDefault', + output: ` + const test = (a: string) => a; + `, + }, + ], + }, + ], + }, + // Multiple properties, one with useless default + { + code: ` + function Bar({ foo = '', bar }: { foo: string; bar: number }) { + return foo + bar; + } + `, + errors: [ + { + data: { type: 'property' }, + line: 2, + messageId: 'uselessDefaultAssignment', + suggestions: [ + { + messageId: 'suggestRemoveDefault', + output: ` + function Bar({ foo, bar }: { foo: string; bar: number }) { + return foo + bar; + } + `, + }, + ], + }, + ], + }, + // Function parameter with null (but not undefined) - default is useless + { + code: ` + function test(a: string | null = 'default') { + return a; + } + `, + errors: [ + { + data: { type: 'parameter' }, + line: 2, + messageId: 'uselessDefaultAssignment', + suggestions: [ + { + messageId: 'suggestRemoveDefault', + output: ` + function test(a: string | null) { + return a; + } + `, + }, + ], + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-useless-default-assignment.shot b/packages/eslint-plugin/tests/schema-snapshots/no-useless-default-assignment.shot new file mode 100644 index 000000000000..e244625f519b --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/no-useless-default-assignment.shot @@ -0,0 +1,25 @@ + +# SCHEMA: + +[ + { + "additionalProperties": false, + "properties": { + "allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing": { + "description": "Unless this is set to `true`, the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`.", + "type": "boolean" + } + }, + "type": "object" + } +] + + +# TYPES: + +type Options = [ + { + /** Unless this is set to `true`, the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`. */ + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; + }, +]; From 4a8ef6bd03eec69af9771b9edd365662b6b8c508 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sat, 25 Oct 2025 22:58:26 +0200 Subject: [PATCH 02/35] support function parameters without type annotation --- .../rules/no-useless-default-assignment.ts | 66 +++++++++++++++---- .../no-useless-default-assignment.test.ts | 6 ++ 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index ac35e536184c..a7adcb8a8a22 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -103,23 +103,65 @@ export default createRule({ return checker.getTypeOfSymbol(symbol); } - function checkAssignmentPattern(node: TSESTree.AssignmentPattern): void { - const parent = node.parent; - - // Handle direct function parameters (not destructured) + function shouldSkipParameterCheck( + node: TSESTree.AssignmentPattern, + functionNode: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression, + ): boolean { + // If the parameter has an explicit type annotation, don't skip if ( - parent.type === AST_NODE_TYPES.FunctionExpression || - parent.type === AST_NODE_TYPES.ArrowFunctionExpression || - parent.type === AST_NODE_TYPES.FunctionDeclaration + node.left.type !== AST_NODE_TYPES.Identifier || + node.left.typeAnnotation ) { - const tsNode = services.esTreeNodeToTSNodeMap.get(node.left); - const type = checker.getTypeAtLocation(tsNode); - if (!canBeUndefined(type)) { - reportUselessDefault(node, 'parameter'); + return false; + } + + // Only skip the check for function declarations and named function expressions + // that are not callbacks (i.e., they are statements or variable initializers). + // For callbacks, TypeScript infers the type from context, so we should still + // check if the default is useless. + return ( + functionNode.type === AST_NODE_TYPES.FunctionDeclaration || + (functionNode.type === AST_NODE_TYPES.FunctionExpression && + functionNode.parent.type === AST_NODE_TYPES.VariableDeclarator) + ); + } + + function checkAssignmentPattern(node: TSESTree.AssignmentPattern): void { + let current: TSESTree.Node | undefined = node.parent; + + // For function parameters, the AssignmentPattern might be directly in the params array + // So we need to check if any ancestor is a function + while (current != null) { + if ( + current.type === AST_NODE_TYPES.FunctionExpression || + current.type === AST_NODE_TYPES.ArrowFunctionExpression || + current.type === AST_NODE_TYPES.FunctionDeclaration + ) { + // Check if this AssignmentPattern is a direct parameter (not in a destructuring) + const isDirectParameter = current.params.some( + param => param === node, + ); + + if (isDirectParameter) { + if (!shouldSkipParameterCheck(node, current)) { + const tsNode = services.esTreeNodeToTSNodeMap.get(node.left); + const type = checker.getTypeAtLocation(tsNode); + if (!canBeUndefined(type)) { + reportUselessDefault(node, 'parameter'); + } + } + return; + } + break; } - return; + current = current.parent; } + const parent = node.parent; + // Handle destructuring patterns if (parent.type === AST_NODE_TYPES.Property) { // This is a property in an object destructuring pattern diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 73ded6c716a2..da79a58f9ebb 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -76,6 +76,12 @@ ruleTester.run('no-useless-default-assignment', rule, { return a; } `, + // Function parameter without type annotation - default makes it optional + ` + function test(a = 5) { + return a; + } + `, ], invalid: [ // React props destructuring - property is not optional From 7637497c2c4a712d273cabd4dfce15111cc1490a Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sat, 25 Oct 2025 23:17:40 +0200 Subject: [PATCH 03/35] fix false positives --- .../rules/no-useless-default-assignment.mdx | 20 +--- .../rules/no-useless-default-assignment.ts | 70 ++++---------- .../no-useless-default-assignment.shot | 22 +---- .../no-useless-default-assignment.test.ts | 96 ------------------- 4 files changed, 21 insertions(+), 187 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx index 811bf8c4f1c4..7b5eac7d3da2 100644 --- a/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx +++ b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx @@ -25,12 +25,6 @@ function Bar({ foo = '' }: { foo: string }) { const { foo = '' } = { foo: 'bar' }; [1, 2, 3].map((a = 42) => a + 1); - -function test(a: string = 'default') { - return a; -} - -const test = (a: string = 'default') => a; ``` @@ -41,25 +35,13 @@ function Bar({ foo = '' }: { foo?: string }) { return foo; } -[1, 2, 3, undefined].map((a = 42) => a + 1); - const obj: { a?: string } = {}; const { a = 'default' } = obj; const obj: { a: string | undefined } = { a: undefined }; const { a = 'default' } = obj; -function test(a: string | undefined = 'default') { - return a; -} - -function test(a: any = 'default') { - return a; -} - -function test(a: unknown = 'default') { - return a; -} +[1, 2, 3, undefined].map((a = 42) => a + 1); ``` diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index a7adcb8a8a22..e39fd562157a 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -103,65 +103,33 @@ export default createRule({ return checker.getTypeOfSymbol(symbol); } - function shouldSkipParameterCheck( - node: TSESTree.AssignmentPattern, - functionNode: - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression, - ): boolean { - // If the parameter has an explicit type annotation, don't skip - if ( - node.left.type !== AST_NODE_TYPES.Identifier || - node.left.typeAnnotation - ) { - return false; - } - - // Only skip the check for function declarations and named function expressions - // that are not callbacks (i.e., they are statements or variable initializers). - // For callbacks, TypeScript infers the type from context, so we should still - // check if the default is useless. - return ( - functionNode.type === AST_NODE_TYPES.FunctionDeclaration || - (functionNode.type === AST_NODE_TYPES.FunctionExpression && - functionNode.parent.type === AST_NODE_TYPES.VariableDeclarator) - ); - } - function checkAssignmentPattern(node: TSESTree.AssignmentPattern): void { - let current: TSESTree.Node | undefined = node.parent; - - // For function parameters, the AssignmentPattern might be directly in the params array - // So we need to check if any ancestor is a function - while (current != null) { - if ( - current.type === AST_NODE_TYPES.FunctionExpression || - current.type === AST_NODE_TYPES.ArrowFunctionExpression || - current.type === AST_NODE_TYPES.FunctionDeclaration - ) { - // Check if this AssignmentPattern is a direct parameter (not in a destructuring) - const isDirectParameter = current.params.some( - param => param === node, - ); + const parent = node.parent; - if (isDirectParameter) { - if (!shouldSkipParameterCheck(node, current)) { - const tsNode = services.esTreeNodeToTSNodeMap.get(node.left); - const type = checker.getTypeAtLocation(tsNode); - if (!canBeUndefined(type)) { - reportUselessDefault(node, 'parameter'); + // Handle callback parameters (like array.map((a = 42) => ...)) + if ( + parent.type === AST_NODE_TYPES.ArrowFunctionExpression || + parent.type === AST_NODE_TYPES.FunctionExpression + ) { + const paramIndex = parent.params.indexOf(node); + if (paramIndex !== -1) { + const tsFunc = services.esTreeNodeToTSNodeMap.get(parent); + if (ts.isFunctionLike(tsFunc)) { + const signature = checker.getSignatureFromDeclaration(tsFunc); + if (signature) { + const params = signature.getParameters(); + if (paramIndex < params.length) { + const paramType = checker.getTypeOfSymbol(params[paramIndex]); + if (!canBeUndefined(paramType)) { + reportUselessDefault(node, 'parameter'); + } } } - return; } - break; } - current = current.parent; + return; } - const parent = node.parent; - // Handle destructuring patterns if (parent.type === AST_NODE_TYPES.Property) { // This is a property in an object destructuring pattern diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-default-assignment.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-default-assignment.shot index b89f97453a6f..331bda568e3f 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-default-assignment.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-default-assignment.shot @@ -11,36 +11,16 @@ const { foo = '' } = { foo: 'bar' }; [1, 2, 3].map((a = 42) => a + 1); ~~ Default value is useless because the parameter is not optional. -function test(a: string = 'default') { - ~~~~~~~~~ Default value is useless because the parameter is not optional. - return a; -} - -const test = (a: string = 'default') => a; - ~~~~~~~~~ Default value is useless because the parameter is not optional. - Correct function Bar({ foo = '' }: { foo?: string }) { return foo; } -[1, 2, 3, undefined].map((a = 42) => a + 1); - const obj: { a?: string } = {}; const { a = 'default' } = obj; const obj: { a: string | undefined } = { a: undefined }; const { a = 'default' } = obj; -function test(a: string | undefined = 'default') { - return a; -} - -function test(a: any = 'default') { - return a; -} - -function test(a: unknown = 'default') { - return a; -} +[1, 2, 3, undefined].map((a = 42) => a + 1); diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index da79a58f9ebb..2320ec9996da 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -151,101 +151,5 @@ ruleTester.run('no-useless-default-assignment', rule, { }, ], }, - // Function parameter that's not optional - { - code: ` - function test(a: string = 'default') { - return a; - } - `, - errors: [ - { - data: { type: 'parameter' }, - line: 2, - messageId: 'uselessDefaultAssignment', - suggestions: [ - { - messageId: 'suggestRemoveDefault', - output: ` - function test(a: string) { - return a; - } - `, - }, - ], - }, - ], - }, - // Arrow function parameter - { - code: ` - const test = (a: string = 'default') => a; - `, - errors: [ - { - data: { type: 'parameter' }, - line: 2, - messageId: 'uselessDefaultAssignment', - suggestions: [ - { - messageId: 'suggestRemoveDefault', - output: ` - const test = (a: string) => a; - `, - }, - ], - }, - ], - }, - // Multiple properties, one with useless default - { - code: ` - function Bar({ foo = '', bar }: { foo: string; bar: number }) { - return foo + bar; - } - `, - errors: [ - { - data: { type: 'property' }, - line: 2, - messageId: 'uselessDefaultAssignment', - suggestions: [ - { - messageId: 'suggestRemoveDefault', - output: ` - function Bar({ foo, bar }: { foo: string; bar: number }) { - return foo + bar; - } - `, - }, - ], - }, - ], - }, - // Function parameter with null (but not undefined) - default is useless - { - code: ` - function test(a: string | null = 'default') { - return a; - } - `, - errors: [ - { - data: { type: 'parameter' }, - line: 2, - messageId: 'uselessDefaultAssignment', - suggestions: [ - { - messageId: 'suggestRemoveDefault', - output: ` - function test(a: string | null) { - return a; - } - `, - }, - ], - }, - ], - }, ], }); From 7a7b3345281ec6b0efbb7d9efef2a6488f7ffdd4 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sun, 26 Oct 2025 00:00:05 +0200 Subject: [PATCH 04/35] fixing more false positives --- .../rules/no-useless-default-assignment.ts | 20 +++++++ .../no-useless-default-assignment.test.ts | 53 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index e39fd562157a..2c4506fba7d0 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -103,6 +103,21 @@ export default createRule({ return checker.getTypeOfSymbol(symbol); } + function isCallbackFunction( + functionNode: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression, + ): boolean { + const parentType = functionNode.parent.type; + return ( + parentType !== AST_NODE_TYPES.MethodDefinition && + parentType !== AST_NODE_TYPES.VariableDeclarator && + parentType !== AST_NODE_TYPES.Property && + parentType !== AST_NODE_TYPES.ExpressionStatement && + parentType !== AST_NODE_TYPES.ReturnStatement + ); + } + function checkAssignmentPattern(node: TSESTree.AssignmentPattern): void { const parent = node.parent; @@ -113,6 +128,11 @@ export default createRule({ ) { const paramIndex = parent.params.indexOf(node); if (paramIndex !== -1) { + // Only check if this is actually a callback, not a regular function + if (!isCallbackFunction(parent)) { + return; + } + const tsFunc = services.esTreeNodeToTSNodeMap.get(parent); if (ts.isFunctionLike(tsFunc)) { const signature = checker.getSignatureFromDeclaration(tsFunc); diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 2320ec9996da..1464784cdf2a 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -47,6 +47,24 @@ ruleTester.run('no-useless-default-assignment', rule, { return a; } `, + // Function parameter as arrow function with non-optional type + ` + (a: string = 'default') => a; + `, + // Function parameter with non-optional type + ` + function test(a: string = 'default') { + return a; + } + `, + // Class method parameter with non-optional type + ` + class C { + public test(a: string = 'default') { + return a; + } + } + `, // Destructuring with union type including undefined ` const obj: { a: string | undefined } = { a: undefined }; @@ -82,6 +100,12 @@ ruleTester.run('no-useless-default-assignment', rule, { return a; } `, + // Don't use default values for parameters without type annotations + ` + function createValidator(): () => void { + return (param = 5) => {}; + } + `, ], invalid: [ // React props destructuring - property is not optional @@ -109,6 +133,35 @@ ruleTester.run('no-useless-default-assignment', rule, { }, ], }, + // Class method parameter - property is not optional + { + code: ` + class C { + public method({ foo = '' }: { foo: string }) { + return foo; + } + } + `, + errors: [ + { + data: { type: 'property' }, + line: 3, + messageId: 'uselessDefaultAssignment', + suggestions: [ + { + messageId: 'suggestRemoveDefault', + output: ` + class C { + public method({ foo }: { foo: string }) { + return foo; + } + } + `, + }, + ], + }, + ], + }, // Destructuring assignment - property is not optional { code: ` From 527b3670ca0f555642d6bf4137dd4c0ba9a0344c Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sun, 26 Oct 2025 00:00:20 +0200 Subject: [PATCH 05/35] enable rule in repo to dogfood --- eslint.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 1c27183e58f5..6aca18538223 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -162,6 +162,7 @@ export default defineConfig( varsIgnorePattern: '^_', }, ], + '@typescript-eslint/no-useless-default-assignment': 'error', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/prefer-literal-enum-member': [ 'error', From 010c166b9a3dbfb62b0b26d9ae7a204fe5449b5a Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sun, 26 Oct 2025 00:11:13 +0200 Subject: [PATCH 06/35] cleanup code and increase code coverage --- .../rules/no-useless-default-assignment.ts | 29 +++---------------- .../no-useless-default-assignment.test.ts | 5 ++++ 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index 2c4506fba7d0..fc79192943a9 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -153,10 +153,7 @@ export default createRule({ // Handle destructuring patterns if (parent.type === AST_NODE_TYPES.Property) { // This is a property in an object destructuring pattern - const objectPattern = parent.parent as TSESTree.ObjectPattern | null; - if (!objectPattern) { - return; - } + const objectPattern = parent.parent as TSESTree.ObjectPattern; // Get the source type being destructured const sourceType = getSourceTypeForPattern(objectPattern); @@ -214,25 +211,12 @@ export default createRule({ const paramIndex = parent.params.indexOf( currentNode as TSESTree.Parameter, ); - if (paramIndex === -1) { - return null; - } - const tsFunc = services.esTreeNodeToTSNodeMap.get(parent); - if (!ts.isFunctionLike(tsFunc)) { - return null; - } - const signature = checker.getSignatureFromDeclaration(tsFunc); if (!signature) { return null; } - const params = signature.getParameters(); - if (paramIndex >= params.length) { - return null; - } - return checker.getTypeOfSymbol(params[paramIndex]); } @@ -264,16 +248,11 @@ export default createRule({ { messageId: 'suggestRemoveDefault', fix(fixer) { - // Remove the ` = default_value` part - const leftNode = node.left; - const tokenBefore = context.sourceCode.getTokenBefore(node.right); - if (!tokenBefore?.value || tokenBefore.value !== '=') { - return null; - } // Remove from before the = to the end of the default value // Find the start position (including whitespace before =) - const leftEnd = leftNode.range[1]; - return fixer.removeRange([leftEnd, node.range[1]]); + const start = node.left.range[1]; + const end = node.range[1]; + return fixer.removeRange([start, end]); }, }, ], diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 1464784cdf2a..da197dacd2a1 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -41,6 +41,11 @@ ruleTester.run('no-useless-default-assignment', rule, { const obj: { a?: string } = {}; const { a = 'default' } = obj; `, + // Object destructuring with optional property + ` + const obj: { 'literal-key': string } = { 'literal-key': 'value' }; + const { 'literal-key': literalKey = 'default' } = obj; + `, // Function parameter with union including undefined ` function test(a: string | undefined = 'default') { From ff7bd0bd8d66530cb49d3973428b2dcfbcbc7f63 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sun, 26 Oct 2025 00:22:31 +0200 Subject: [PATCH 07/35] cleanup code and add column numbers to tests --- .../rules/no-useless-default-assignment.ts | 3 - .../no-useless-default-assignment.test.ts | 57 ++++++++++--------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index fc79192943a9..468a730fc047 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -155,19 +155,16 @@ export default createRule({ // This is a property in an object destructuring pattern const objectPattern = parent.parent as TSESTree.ObjectPattern; - // Get the source type being destructured const sourceType = getSourceTypeForPattern(objectPattern); if (!sourceType) { return; } - // Get the property name const propertyName = getPropertyName(parent.key); if (!propertyName) { return; } - // Get the type of this specific property const propertyType = getPropertyType(sourceType, propertyName); if (!propertyType) { return; diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index da197dacd2a1..488e284b49c3 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -16,53 +16,48 @@ const ruleTester = new RuleTester({ ruleTester.run('no-useless-default-assignment', rule, { valid: [ - // React props destructuring with optional property + // Default is valid when property type is optional ` function Bar({ foo = '' }: { foo?: string }) { return foo; } `, - // Destructuring assignment - no default needed when property exists + // No default value used in destructuring ` const { foo } = { foo: 'bar' }; `, - // Default parameter with undefined in union + // Default is valid when array contains undefined ` [1, 2, 3, undefined].map((a = 42) => a + 1); `, - // Default parameter with optional parameter type + // Default is valid for optional parameter ` function test(a?: number) { return a; } `, - // Object destructuring with optional property + // Default is valid when destructuring optional property ` const obj: { a?: string } = {}; const { a = 'default' } = obj; `, - // Object destructuring with optional property - ` - const obj: { 'literal-key': string } = { 'literal-key': 'value' }; - const { 'literal-key': literalKey = 'default' } = obj; - `, - // Function parameter with union including undefined + // Default is valid when union type includes undefined ` function test(a: string | undefined = 'default') { return a; } `, - // Function parameter as arrow function with non-optional type + // Default on arrow function parameter is not checked ` (a: string = 'default') => a; `, - // Function parameter with non-optional type + // Default on regular function parameter is not checked ` function test(a: string = 'default') { return a; } `, - // Class method parameter with non-optional type + // Default on class method parameter is not checked ` class C { public test(a: string = 'default') { @@ -70,42 +65,42 @@ ruleTester.run('no-useless-default-assignment', rule, { } } `, - // Destructuring with union type including undefined + // Default is valid when union type includes undefined ` const obj: { a: string | undefined } = { a: undefined }; const { a = 'default' } = obj; `, - // Array parameter that can be undefined + // Default is valid when union type includes undefined ` function test(arr: number[] | undefined = []) { return arr; } `, - // Complex destructuring with optional + // Default is valid for nested optional properties ` function Bar({ nested: { foo = '' } = {} }: { nested?: { foo?: string } }) { return foo; } `, - // Default with any type (should not error) + // Default is valid for any type ` function test(a: any = 'default') { return a; } `, - // Default with unknown type (should not error) + // Default is valid for unknown type ` function test(a: unknown = 'default') { return a; } `, - // Function parameter without type annotation - default makes it optional + // Default on parameter without type annotation is not checked ` function test(a = 5) { return a; } `, - // Don't use default values for parameters without type annotations + // Default on callback parameter without type annotation is not checked ` function createValidator(): () => void { return (param = 5) => {}; @@ -113,7 +108,7 @@ ruleTester.run('no-useless-default-assignment', rule, { `, ], invalid: [ - // React props destructuring - property is not optional + // Default is useless when property type is required { code: ` function Bar({ foo = '' }: { foo: string }) { @@ -122,7 +117,9 @@ ruleTester.run('no-useless-default-assignment', rule, { `, errors: [ { + column: 30, data: { type: 'property' }, + endColumn: 32, line: 2, messageId: 'uselessDefaultAssignment', suggestions: [ @@ -138,7 +135,7 @@ ruleTester.run('no-useless-default-assignment', rule, { }, ], }, - // Class method parameter - property is not optional + // Default is useless when destructured property type is required { code: ` class C { @@ -149,7 +146,9 @@ ruleTester.run('no-useless-default-assignment', rule, { `, errors: [ { + column: 33, data: { type: 'property' }, + endColumn: 35, line: 3, messageId: 'uselessDefaultAssignment', suggestions: [ @@ -167,35 +166,39 @@ ruleTester.run('no-useless-default-assignment', rule, { }, ], }, - // Destructuring assignment - property is not optional + // Default is useless when destructuring required property with literal key { code: ` - const { foo = '' } = { foo: 'bar' }; + const { 'literal-key': literalKey = 'default' } = { 'literal-key': 'value' }; `, errors: [ { + column: 45, data: { type: 'property' }, + endColumn: 54, line: 2, messageId: 'uselessDefaultAssignment', suggestions: [ { messageId: 'suggestRemoveDefault', output: ` - const { foo } = { foo: 'bar' }; + const { 'literal-key': literalKey } = { 'literal-key': 'value' }; `, }, ], }, ], }, - // Default parameter - array elements are never undefined + // Default is useless when array elements cannot be undefined { code: ` [1, 2, 3].map((a = 42) => a + 1); `, errors: [ { + column: 28, data: { type: 'parameter' }, + endColumn: 30, line: 2, messageId: 'uselessDefaultAssignment', suggestions: [ From 8d29d31d0da8cb013ab334b5fae4208b1cdfe587 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sun, 26 Oct 2025 00:43:48 +0200 Subject: [PATCH 08/35] add tests to increase test coverage --- .../no-useless-default-assignment.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 488e284b49c3..1a71e1687eba 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -1,4 +1,5 @@ import { RuleTester } from '@typescript-eslint/rule-tester'; +import * as path from 'node:path'; import rule from '../../src/rules/no-useless-default-assignment'; import { getFixturesRootDir } from '../RuleTester'; @@ -106,6 +107,18 @@ ruleTester.run('no-useless-default-assignment', rule, { return (param = 5) => {}; } `, + // Default is valid when property type is any + ` + function Bar({ foo = '' }: { foo: any }) { + return foo; + } + `, + // Default is valid when property type is unknown + ` + function Bar({ foo = '' }: { foo: unknown }) { + return foo; + } + `, ], invalid: [ // Default is useless when property type is required @@ -212,5 +225,23 @@ ruleTester.run('no-useless-default-assignment', rule, { }, ], }, + { + code: '', + errors: [ + { + messageId: 'noStrictNullCheck', + }, + ], + languageOptions: { + parserOptions: { + tsconfigRootDir: path.join(rootPath, 'unstrict'), + }, + }, + options: [ + { + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, + }, + ], + }, ], }); From df51d1c594429b850f083a0ae0cc5e9c43b5bb7d Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Tue, 4 Nov 2025 17:03:31 +0100 Subject: [PATCH 09/35] remove option allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing --- .../rules/no-useless-default-assignment.mdx | 18 ------- .../rules/no-useless-default-assignment.ts | 50 ++----------------- .../no-useless-default-assignment.test.ts | 19 ------- 3 files changed, 5 insertions(+), 82 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx index 7b5eac7d3da2..9b9b94b9df07 100644 --- a/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx +++ b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx @@ -47,24 +47,6 @@ const { a = 'default' } = obj; -## Options - -### `allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing` - -{/* insert option description */} - -:::danger Deprecated -This option will be removed in the next major version of typescript-eslint. -::: - -If this is set to `false`, then the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`. - -Without `strictNullChecks`, TypeScript essentially erases `undefined` and `null` from the types. This means when this rule inspects the types from a variable, **it will not be able to tell that the variable might be `null` or `undefined`**, which essentially makes this rule useless. - -You should be using `strictNullChecks` to ensure complete type-safety in your codebase. - -If for some reason you cannot turn on `strictNullChecks`, but still want to use this rule - you can use this option to allow it - but know that the behavior of this rule is _undefined_ with the compiler option turned off. We will not accept bug reports if you are using this option. - ## When Not To Use It If your project is not accurately typed or `strictNullChecks` is disabled. diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index 468a730fc047..93a59aaab4a6 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -12,18 +12,9 @@ import { isTypeUnknownType, } from '../util'; -type Options = [ - { - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; - }, -]; - -type MessageId = - | 'noStrictNullCheck' - | 'suggestRemoveDefault' - | 'uselessDefaultAssignment'; +type MessageId = 'suggestRemoveDefault' | 'uselessDefaultAssignment'; -export default createRule({ +export default createRule<[], MessageId>({ name: 'no-useless-default-assignment', meta: { type: 'suggestion', @@ -33,8 +24,6 @@ export default createRule({ }, hasSuggestions: true, messages: { - noStrictNullCheck: - 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', suggestRemoveDefault: 'Remove useless default value', uselessDefaultAssignment: 'Default value is useless because the {{ type }} is not optional.', @@ -43,43 +32,14 @@ export default createRule({ { type: 'object', additionalProperties: false, - properties: { - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: { - type: 'boolean', - description: - 'Unless this is set to `true`, the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`.', - }, - }, + properties: {}, }, ], }, - defaultOptions: [ - { - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, - }, - ], - create(context, [options]) { + defaultOptions: [], + create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); - const compilerOptions = services.program.getCompilerOptions(); - - const isStrictNullChecks = tsutils.isStrictCompilerOptionEnabled( - compilerOptions, - 'strictNullChecks', - ); - - if ( - !isStrictNullChecks && - options.allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true - ) { - context.report({ - loc: { - start: { column: 0, line: 0 }, - end: { column: 0, line: 0 }, - }, - messageId: 'noStrictNullCheck', - }); - } function canBeUndefined(type: ts.Type): boolean { // any and unknown can be undefined diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 1a71e1687eba..98605a9c8be9 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -1,5 +1,4 @@ import { RuleTester } from '@typescript-eslint/rule-tester'; -import * as path from 'node:path'; import rule from '../../src/rules/no-useless-default-assignment'; import { getFixturesRootDir } from '../RuleTester'; @@ -225,23 +224,5 @@ ruleTester.run('no-useless-default-assignment', rule, { }, ], }, - { - code: '', - errors: [ - { - messageId: 'noStrictNullCheck', - }, - ], - languageOptions: { - parserOptions: { - tsconfigRootDir: path.join(rootPath, 'unstrict'), - }, - }, - options: [ - { - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, - }, - ], - }, ], }); From 0526062db07ebaa7cf031902731f80a6d6f09404 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Tue, 4 Nov 2025 17:04:39 +0100 Subject: [PATCH 10/35] remove comments at each test case --- .../no-useless-default-assignment.test.ts | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 98605a9c8be9..3992ac769c2e 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -16,48 +16,39 @@ const ruleTester = new RuleTester({ ruleTester.run('no-useless-default-assignment', rule, { valid: [ - // Default is valid when property type is optional ` function Bar({ foo = '' }: { foo?: string }) { return foo; } `, - // No default value used in destructuring ` const { foo } = { foo: 'bar' }; `, - // Default is valid when array contains undefined ` [1, 2, 3, undefined].map((a = 42) => a + 1); `, - // Default is valid for optional parameter ` function test(a?: number) { return a; } `, - // Default is valid when destructuring optional property ` const obj: { a?: string } = {}; const { a = 'default' } = obj; `, - // Default is valid when union type includes undefined ` function test(a: string | undefined = 'default') { return a; } `, - // Default on arrow function parameter is not checked ` (a: string = 'default') => a; `, - // Default on regular function parameter is not checked ` function test(a: string = 'default') { return a; } `, - // Default on class method parameter is not checked ` class C { public test(a: string = 'default') { @@ -65,54 +56,45 @@ ruleTester.run('no-useless-default-assignment', rule, { } } `, - // Default is valid when union type includes undefined ` const obj: { a: string | undefined } = { a: undefined }; const { a = 'default' } = obj; `, - // Default is valid when union type includes undefined ` function test(arr: number[] | undefined = []) { return arr; } `, - // Default is valid for nested optional properties ` function Bar({ nested: { foo = '' } = {} }: { nested?: { foo?: string } }) { return foo; } `, - // Default is valid for any type ` function test(a: any = 'default') { return a; } `, - // Default is valid for unknown type ` function test(a: unknown = 'default') { return a; } `, - // Default on parameter without type annotation is not checked ` function test(a = 5) { return a; } `, - // Default on callback parameter without type annotation is not checked ` function createValidator(): () => void { return (param = 5) => {}; } `, - // Default is valid when property type is any ` function Bar({ foo = '' }: { foo: any }) { return foo; } `, - // Default is valid when property type is unknown ` function Bar({ foo = '' }: { foo: unknown }) { return foo; @@ -120,7 +102,6 @@ ruleTester.run('no-useless-default-assignment', rule, { `, ], invalid: [ - // Default is useless when property type is required { code: ` function Bar({ foo = '' }: { foo: string }) { @@ -147,7 +128,6 @@ ruleTester.run('no-useless-default-assignment', rule, { }, ], }, - // Default is useless when destructured property type is required { code: ` class C { @@ -178,7 +158,6 @@ ruleTester.run('no-useless-default-assignment', rule, { }, ], }, - // Default is useless when destructuring required property with literal key { code: ` const { 'literal-key': literalKey = 'default' } = { 'literal-key': 'value' }; @@ -201,7 +180,6 @@ ruleTester.run('no-useless-default-assignment', rule, { }, ], }, - // Default is useless when array elements cannot be undefined { code: ` [1, 2, 3].map((a = 42) => a + 1); From 90d121a164ce0bc973e3e0d199bfaf90a9366821 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Tue, 4 Nov 2025 17:05:36 +0100 Subject: [PATCH 11/35] remove rule from rulesWithComplexOptionHeadings --- packages/eslint-plugin/tests/docs.test.mts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/tests/docs.test.mts b/packages/eslint-plugin/tests/docs.test.mts index 3f3725e1ff9f..ea12c3e02179 100644 --- a/packages/eslint-plugin/tests/docs.test.mts +++ b/packages/eslint-plugin/tests/docs.test.mts @@ -196,7 +196,6 @@ describe('Validating rule docs', () => { 'switch-exhaustiveness-check', 'unbound-method', 'no-unnecessary-boolean-literal-compare', - 'no-useless-default-assignment', ]); it('All rules must have a corresponding rule doc', async () => { From 9b55fb2eb2f8a2f7352fc24daa48fdf556e1172a Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Tue, 4 Nov 2025 17:09:55 +0100 Subject: [PATCH 12/35] update snapshot --- .../no-useless-default-assignment.shot | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-useless-default-assignment.shot b/packages/eslint-plugin/tests/schema-snapshots/no-useless-default-assignment.shot index e244625f519b..f96a41f8ec0f 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-useless-default-assignment.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-useless-default-assignment.shot @@ -4,12 +4,7 @@ [ { "additionalProperties": false, - "properties": { - "allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing": { - "description": "Unless this is set to `true`, the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`.", - "type": "boolean" - } - }, + "properties": {}, "type": "object" } ] @@ -17,9 +12,4 @@ # TYPES: -type Options = [ - { - /** Unless this is set to `true`, the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`. */ - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; - }, -]; +type Options = [{}]; From 422a7049b296ecb01a7169ff71ef1b064465d1fc Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Tue, 4 Nov 2025 17:10:54 +0100 Subject: [PATCH 13/35] Update packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- .../eslint-plugin/docs/rules/no-useless-default-assignment.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx index 9b9b94b9df07..5db2ccdc94c3 100644 --- a/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx +++ b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx @@ -49,4 +49,5 @@ const { a = 'default' } = obj; ## When Not To Use It -If your project is not accurately typed or `strictNullChecks` is disabled. +If your codebase is still onboarding to TypeScript and/or has many existing `any`s or areas of loosely typed code, it may be difficult to enable this rule. +You might consider using ESLint disable comments for those specific situations instead of completely disabling this rule. From 16ff0d66a8d4c025bfd07146a4a8d438939d6100 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Tue, 4 Nov 2025 17:11:51 +0100 Subject: [PATCH 14/35] Update packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- .../docs/rules/no-useless-default-assignment.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx index 5db2ccdc94c3..e35d07545a12 100644 --- a/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx +++ b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx @@ -9,8 +9,9 @@ import TabItem from '@theme/TabItem'; > > See **https://typescript-eslint.io/rules/no-useless-default-assignment** for documentation. -[Default parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters) and [default values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#default_value) are only used if the parameter or property is `undefined` (either because its value is `undefined` or because it is missing). -If the type of a parameter or property can never be `undefined`, then the default is never actually used. +[Default parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters) and [default values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#default_value) are only used if the parameter or property is `undefined`. +That can happen when a value is missing, or when one is provided and set to `undefined`. +If a non-`undefined` value is guaranteed to be provided, then there is no need to define a default. ## Examples From 4c40bfb4e41d1e21783d585cb40f98c98eac78f3 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Tue, 4 Nov 2025 17:25:47 +0100 Subject: [PATCH 15/35] add rule to strict config --- .../src/configs/eslintrc/strict-type-checked-only.ts | 1 + .../eslint-plugin/src/configs/eslintrc/strict-type-checked.ts | 1 + .../eslint-plugin/src/configs/flat/strict-type-checked-only.ts | 1 + packages/eslint-plugin/src/configs/flat/strict-type-checked.ts | 1 + .../eslint-plugin/src/rules/no-useless-default-assignment.ts | 1 + 5 files changed, 5 insertions(+) diff --git a/packages/eslint-plugin/src/configs/eslintrc/strict-type-checked-only.ts b/packages/eslint-plugin/src/configs/eslintrc/strict-type-checked-only.ts index a2b5b457d01e..c455ac88371b 100644 --- a/packages/eslint-plugin/src/configs/eslintrc/strict-type-checked-only.ts +++ b/packages/eslint-plugin/src/configs/eslintrc/strict-type-checked-only.ts @@ -39,6 +39,7 @@ export = { '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', '@typescript-eslint/no-unsafe-unary-minus': 'error', + '@typescript-eslint/no-useless-default-assignment': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error', 'prefer-promise-reject-errors': 'off', diff --git a/packages/eslint-plugin/src/configs/eslintrc/strict-type-checked.ts b/packages/eslint-plugin/src/configs/eslintrc/strict-type-checked.ts index ec1523d8b48e..aba8c3006a1a 100644 --- a/packages/eslint-plugin/src/configs/eslintrc/strict-type-checked.ts +++ b/packages/eslint-plugin/src/configs/eslintrc/strict-type-checked.ts @@ -68,6 +68,7 @@ export = { '@typescript-eslint/no-unused-vars': 'error', 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/no-useless-default-assignment': 'error', '@typescript-eslint/no-wrapper-object-types': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error', diff --git a/packages/eslint-plugin/src/configs/flat/strict-type-checked-only.ts b/packages/eslint-plugin/src/configs/flat/strict-type-checked-only.ts index 043f1aad0cd5..2cddf49a4633 100644 --- a/packages/eslint-plugin/src/configs/flat/strict-type-checked-only.ts +++ b/packages/eslint-plugin/src/configs/flat/strict-type-checked-only.ts @@ -52,6 +52,7 @@ export default ( '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', '@typescript-eslint/no-unsafe-unary-minus': 'error', + '@typescript-eslint/no-useless-default-assignment': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error', 'prefer-promise-reject-errors': 'off', diff --git a/packages/eslint-plugin/src/configs/flat/strict-type-checked.ts b/packages/eslint-plugin/src/configs/flat/strict-type-checked.ts index b3c0a2391178..4d34eb4925d8 100644 --- a/packages/eslint-plugin/src/configs/flat/strict-type-checked.ts +++ b/packages/eslint-plugin/src/configs/flat/strict-type-checked.ts @@ -81,6 +81,7 @@ export default ( '@typescript-eslint/no-unused-vars': 'error', 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/no-useless-default-assignment': 'error', '@typescript-eslint/no-wrapper-object-types': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error', diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index 93a59aaab4a6..51e12f1a5e37 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -20,6 +20,7 @@ export default createRule<[], MessageId>({ type: 'suggestion', docs: { description: 'Disallow default values that will never be used', + recommended: 'strict', requiresTypeChecking: true, }, hasSuggestions: true, From 6bef7faf57b01230b3f49dd99bbfc4851576d1d9 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Wed, 5 Nov 2025 20:11:28 +0100 Subject: [PATCH 16/35] turn suggestion into fix --- .../rules/no-useless-default-assignment.ts | 24 ++++------ .../no-useless-default-assignment.test.ts | 44 +++++-------------- 2 files changed, 21 insertions(+), 47 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index 51e12f1a5e37..4bc7b2c0aa7b 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -12,7 +12,7 @@ import { isTypeUnknownType, } from '../util'; -type MessageId = 'suggestRemoveDefault' | 'uselessDefaultAssignment'; +type MessageId = 'uselessDefaultAssignment'; export default createRule<[], MessageId>({ name: 'no-useless-default-assignment', @@ -23,9 +23,8 @@ export default createRule<[], MessageId>({ recommended: 'strict', requiresTypeChecking: true, }, - hasSuggestions: true, + fixable: 'code', messages: { - suggestRemoveDefault: 'Remove useless default value', uselessDefaultAssignment: 'Default value is useless because the {{ type }} is not optional.', }, @@ -202,18 +201,13 @@ export default createRule<[], MessageId>({ node: node.right, messageId: 'uselessDefaultAssignment', data: { type }, - suggest: [ - { - messageId: 'suggestRemoveDefault', - fix(fixer) { - // Remove from before the = to the end of the default value - // Find the start position (including whitespace before =) - const start = node.left.range[1]; - const end = node.range[1]; - return fixer.removeRange([start, end]); - }, - }, - ], + fix(fixer) { + // Remove from before the = to the end of the default value + // Find the start position (including whitespace before =) + const start = node.left.range[1]; + const end = node.range[1]; + return fixer.removeRange([start, end]); + }, }); } diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 3992ac769c2e..dfbdcc7024ac 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -115,18 +115,13 @@ ruleTester.run('no-useless-default-assignment', rule, { endColumn: 32, line: 2, messageId: 'uselessDefaultAssignment', - suggestions: [ - { - messageId: 'suggestRemoveDefault', - output: ` + }, + ], + output: ` function Bar({ foo }: { foo: string }) { return foo; } `, - }, - ], - }, - ], }, { code: ` @@ -143,20 +138,15 @@ ruleTester.run('no-useless-default-assignment', rule, { endColumn: 35, line: 3, messageId: 'uselessDefaultAssignment', - suggestions: [ - { - messageId: 'suggestRemoveDefault', - output: ` + }, + ], + output: ` class C { public method({ foo }: { foo: string }) { return foo; } } `, - }, - ], - }, - ], }, { code: ` @@ -169,16 +159,11 @@ ruleTester.run('no-useless-default-assignment', rule, { endColumn: 54, line: 2, messageId: 'uselessDefaultAssignment', - suggestions: [ - { - messageId: 'suggestRemoveDefault', - output: ` - const { 'literal-key': literalKey } = { 'literal-key': 'value' }; - `, - }, - ], }, ], + output: ` + const { 'literal-key': literalKey } = { 'literal-key': 'value' }; + `, }, { code: ` @@ -191,16 +176,11 @@ ruleTester.run('no-useless-default-assignment', rule, { endColumn: 30, line: 2, messageId: 'uselessDefaultAssignment', - suggestions: [ - { - messageId: 'suggestRemoveDefault', - output: ` - [1, 2, 3].map((a) => a + 1); - `, - }, - ], }, ], + output: ` + [1, 2, 3].map((a) => a + 1); + `, }, ], }); From 299249e19cf69aac49c5716f848859bf70fb50cd Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Wed, 5 Nov 2025 20:52:58 +0100 Subject: [PATCH 17/35] add more test cases and make them pass --- .../rules/no-useless-default-assignment.ts | 34 ++++++++++ .../no-useless-default-assignment.test.ts | 64 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index 4bc7b2c0aa7b..23da8a23e9e6 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -63,6 +63,20 @@ export default createRule<[], MessageId>({ return checker.getTypeOfSymbol(symbol); } + function getArrayElementType( + arrayType: ts.Type, + elementIndex: number, + ): ts.Type | null { + if (checker.isTupleType(arrayType)) { + const tupleArgs = checker.getTypeArguments(arrayType); + if (elementIndex < tupleArgs.length) { + return tupleArgs[elementIndex]; + } + } + + return null; + } + function isCallbackFunction( functionNode: | TSESTree.ArrowFunctionExpression @@ -133,6 +147,26 @@ export default createRule<[], MessageId>({ if (!canBeUndefined(propertyType)) { reportUselessDefault(node, 'property'); } + } else if (parent.type === AST_NODE_TYPES.ArrayPattern) { + // This is an element in an array destructuring pattern + const sourceType = getSourceTypeForPattern(parent); + if (!sourceType) { + return; + } + + const elementIndex = parent.elements.indexOf(node); + if (elementIndex === -1) { + return; + } + + const elementType = getArrayElementType(sourceType, elementIndex); + if (!elementType) { + return; + } + + if (!canBeUndefined(elementType)) { + reportUselessDefault(node, 'property'); + } } } diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index dfbdcc7024ac..6b164733a426 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -100,6 +100,20 @@ ruleTester.run('no-useless-default-assignment', rule, { return foo; } `, + ` + function getValue(): undefined; + function getValue(box: { value: string }): string; + function getValue({ value = undefined }: { value?: string } = {}): + | string + | undefined { + return value; + } + `, + ` + function getValueObject({ value = '' }: Partial<{ value: string }>) { + return value; + } + `, ], invalid: [ { @@ -182,5 +196,55 @@ ruleTester.run('no-useless-default-assignment', rule, { [1, 2, 3].map((a) => a + 1); `, }, + { + code: ` + function getValue(): undefined; + function getValue(box: { value: string }): string; + function getValue({ value = undefined }: { value: string } = {}): + | string + | undefined { + return value; + } + `, + errors: [ + { + column: 37, + data: { type: 'property' }, + endColumn: 46, + line: 4, + messageId: 'uselessDefaultAssignment', + }, + ], + output: ` + function getValue(): undefined; + function getValue(box: { value: string }): string; + function getValue({ value }: { value: string } = {}): + | string + | undefined { + return value; + } + `, + }, + { + code: ` + function getValue([value = '']: [string]) { + return value; + } + `, + errors: [ + { + column: 36, + data: { type: 'property' }, + endColumn: 38, + line: 2, + messageId: 'uselessDefaultAssignment', + }, + ], + output: ` + function getValue([value]: [string]) { + return value; + } + `, + }, ], }); From 242883a4662e87eb1340c440b1ad8f6b0202f6a7 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Wed, 5 Nov 2025 21:04:03 +0100 Subject: [PATCH 18/35] make example code easier to understand and add tuple example --- .../docs/rules/no-useless-default-assignment.mdx | 8 ++++---- .../no-useless-default-assignment.shot | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx index e35d07545a12..b09fae97dd21 100644 --- a/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx +++ b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx @@ -25,6 +25,8 @@ function Bar({ foo = '' }: { foo: string }) { const { foo = '' } = { foo: 'bar' }; +const [foo = ''] = ['bar']; + [1, 2, 3].map((a = 42) => a + 1); ``` @@ -36,11 +38,9 @@ function Bar({ foo = '' }: { foo?: string }) { return foo; } -const obj: { a?: string } = {}; -const { a = 'default' } = obj; +const { foo = '' } = { foo: undefined }; -const obj: { a: string | undefined } = { a: undefined }; -const { a = 'default' } = obj; +const [foo = ''] = [undefined]; [1, 2, 3, undefined].map((a = 42) => a + 1); ``` diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-default-assignment.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-default-assignment.shot index 331bda568e3f..b63ea9a740f0 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-default-assignment.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-default-assignment.shot @@ -8,6 +8,9 @@ function Bar({ foo = '' }: { foo: string }) { const { foo = '' } = { foo: 'bar' }; ~~ Default value is useless because the property is not optional. +const [foo = ''] = ['bar']; + ~~ Default value is useless because the property is not optional. + [1, 2, 3].map((a = 42) => a + 1); ~~ Default value is useless because the parameter is not optional. @@ -17,10 +20,8 @@ function Bar({ foo = '' }: { foo?: string }) { return foo; } -const obj: { a?: string } = {}; -const { a = 'default' } = obj; +const { foo = '' } = { foo: undefined }; -const obj: { a: string | undefined } = { a: undefined }; -const { a = 'default' } = obj; +const [foo = ''] = [undefined]; [1, 2, 3, undefined].map((a = 42) => a + 1); From 7d7a63686e3bac5fb4bdd7a9d2c74ec0f870e9d6 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Wed, 5 Nov 2025 22:04:27 +0100 Subject: [PATCH 19/35] increase test coverage --- .../rules/no-useless-default-assignment.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 6b164733a426..6c7ea786c53b 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -114,6 +114,20 @@ ruleTester.run('no-useless-default-assignment', rule, { return value; } `, + ` + const { value = 'default' } = someUnknownFunction(); + `, + ` + const [value = 'default'] = someUnknownFunction(); + `, + ` + for (const { value = 'default' } of []) { + } + `, + ` + for (const [value = 'default'] of []) { + } + `, ], invalid: [ { From f5fb05cedf6e6025fae4c6bd48b43a064c711829 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Wed, 19 Nov 2025 17:31:28 +0100 Subject: [PATCH 20/35] remove self-apparent comments --- .../src/rules/no-useless-default-assignment.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index 23da8a23e9e6..7543e9cf19a1 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -42,11 +42,9 @@ export default createRule<[], MessageId>({ const checker = services.program.getTypeChecker(); function canBeUndefined(type: ts.Type): boolean { - // any and unknown can be undefined if (isTypeAnyType(type) || isTypeUnknownType(type)) { return true; } - // Check if any part of the union includes undefined return tsutils .unionConstituents(type) .some(part => isTypeFlagSet(part, ts.TypeFlags.Undefined)); @@ -102,7 +100,6 @@ export default createRule<[], MessageId>({ ) { const paramIndex = parent.params.indexOf(node); if (paramIndex !== -1) { - // Only check if this is actually a callback, not a regular function if (!isCallbackFunction(parent)) { return; } @@ -124,9 +121,7 @@ export default createRule<[], MessageId>({ return; } - // Handle destructuring patterns if (parent.type === AST_NODE_TYPES.Property) { - // This is a property in an object destructuring pattern const objectPattern = parent.parent as TSESTree.ObjectPattern; const sourceType = getSourceTypeForPattern(objectPattern); @@ -148,7 +143,6 @@ export default createRule<[], MessageId>({ reportUselessDefault(node, 'property'); } } else if (parent.type === AST_NODE_TYPES.ArrayPattern) { - // This is an element in an array destructuring pattern const sourceType = getSourceTypeForPattern(parent); if (!sourceType) { return; @@ -187,13 +181,11 @@ export default createRule<[], MessageId>({ const parent = currentNode.parent; - // Handle variable declarator if (parent.type === AST_NODE_TYPES.VariableDeclarator && parent.init) { const tsNode = services.esTreeNodeToTSNodeMap.get(parent.init); return checker.getTypeAtLocation(tsNode); } - // Handle function parameter if ( parent.type === AST_NODE_TYPES.FunctionExpression || parent.type === AST_NODE_TYPES.ArrowFunctionExpression || From ba7e074ef0bd067b1b798e5b6b06cb956bdc1046 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Wed, 19 Nov 2025 17:32:47 +0100 Subject: [PATCH 21/35] use createRuleTesterWithTypes --- .../rules/no-useless-default-assignment.test.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 6c7ea786c53b..41be2ff0dc92 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -1,18 +1,7 @@ -import { RuleTester } from '@typescript-eslint/rule-tester'; - import rule from '../../src/rules/no-useless-default-assignment'; -import { getFixturesRootDir } from '../RuleTester'; - -const rootPath = getFixturesRootDir(); +import { createRuleTesterWithTypes } from '../RuleTester'; -const ruleTester = new RuleTester({ - languageOptions: { - parserOptions: { - project: './tsconfig.json', - tsconfigRootDir: rootPath, - }, - }, -}); +const ruleTester = createRuleTesterWithTypes(); ruleTester.run('no-useless-default-assignment', rule, { valid: [ From f64f7d784225db576f90d3c7c35e20f8d41a49ea Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Wed, 19 Nov 2025 17:35:11 +0100 Subject: [PATCH 22/35] use isFunction --- .../src/rules/no-useless-default-assignment.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index 7543e9cf19a1..9d3729dac989 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -7,6 +7,7 @@ import * as ts from 'typescript'; import { createRule, getParserServices, + isFunction, isTypeAnyType, isTypeFlagSet, isTypeUnknownType, @@ -186,11 +187,7 @@ export default createRule<[], MessageId>({ return checker.getTypeAtLocation(tsNode); } - if ( - parent.type === AST_NODE_TYPES.FunctionExpression || - parent.type === AST_NODE_TYPES.ArrowFunctionExpression || - parent.type === AST_NODE_TYPES.FunctionDeclaration - ) { + if (isFunction(parent)) { const paramIndex = parent.params.indexOf( currentNode as TSESTree.Parameter, ); From 03c49ff310dcc8d29e59684256e19a674c8a3473 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Wed, 19 Nov 2025 17:35:55 +0100 Subject: [PATCH 23/35] remove unnecessary schema item --- .../src/rules/no-useless-default-assignment.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index 9d3729dac989..a86eb31e801a 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -29,13 +29,7 @@ export default createRule<[], MessageId>({ uselessDefaultAssignment: 'Default value is useless because the {{ type }} is not optional.', }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: {}, - }, - ], + schema: [], }, defaultOptions: [], create(context) { From 746f9f703a185e81847558a5f4012f87ee3662b5 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Wed, 19 Nov 2025 17:59:12 +0100 Subject: [PATCH 24/35] update snapshot --- .../no-useless-default-assignment.shot | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-useless-default-assignment.shot b/packages/eslint-plugin/tests/schema-snapshots/no-useless-default-assignment.shot index f96a41f8ec0f..cdd9f8375858 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-useless-default-assignment.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-useless-default-assignment.shot @@ -1,15 +1,10 @@ # SCHEMA: -[ - { - "additionalProperties": false, - "properties": {}, - "type": "object" - } -] +[] # TYPES: -type Options = [{}]; +/** No options declared */ +type Options = []; From a4d42f2abacd6805145a12c2427bf0ca329346e9 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sat, 22 Nov 2025 14:06:10 +0100 Subject: [PATCH 25/35] support nested destructuring --- .../rules/no-useless-default-assignment.ts | 57 ++++++++++++------- .../no-useless-default-assignment.test.ts | 50 ++++++++++++++++ 2 files changed, 88 insertions(+), 19 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index a86eb31e801a..7c30a337c0c7 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -67,7 +67,7 @@ export default createRule<[], MessageId>({ } } - return null; + return arrayType.getNumberIndexType() ?? null; } function isCallbackFunction( @@ -159,32 +159,19 @@ export default createRule<[], MessageId>({ } } - function getSourceTypeForPattern( - pattern: TSESTree.ArrayPattern | TSESTree.ObjectPattern, - ): ts.Type | null { - let currentNode: TSESTree.Node = pattern; - - // Walk up through nested patterns - while ( - currentNode.parent.type === AST_NODE_TYPES.AssignmentPattern || - currentNode.parent.type === AST_NODE_TYPES.Property || - currentNode.parent.type === AST_NODE_TYPES.ObjectPattern || - currentNode.parent.type === AST_NODE_TYPES.ArrayPattern - ) { - currentNode = currentNode.parent; + function getSourceTypeForPattern(pattern: TSESTree.Node): ts.Type | null { + const parent = pattern.parent; + if (!parent) { + return null; } - const parent = currentNode.parent; - if (parent.type === AST_NODE_TYPES.VariableDeclarator && parent.init) { const tsNode = services.esTreeNodeToTSNodeMap.get(parent.init); return checker.getTypeAtLocation(tsNode); } if (isFunction(parent)) { - const paramIndex = parent.params.indexOf( - currentNode as TSESTree.Parameter, - ); + const paramIndex = parent.params.indexOf(pattern as TSESTree.Parameter); const tsFunc = services.esTreeNodeToTSNodeMap.get(parent); const signature = checker.getSignatureFromDeclaration(tsFunc); if (!signature) { @@ -194,6 +181,38 @@ export default createRule<[], MessageId>({ return checker.getTypeOfSymbol(params[paramIndex]); } + if (parent.type === AST_NODE_TYPES.AssignmentPattern) { + return getSourceTypeForPattern(parent); + } + + if (parent.type === AST_NODE_TYPES.Property) { + const objectPattern = parent.parent as TSESTree.ObjectPattern; + const objectType = getSourceTypeForPattern(objectPattern); + if (!objectType) { + return null; + } + const propertyName = getPropertyName(parent.key); + if (!propertyName) { + return null; + } + return getPropertyType(objectType, propertyName); + } + + if (parent.type === AST_NODE_TYPES.ArrayPattern) { + const arrayPattern = parent; + const arrayType = getSourceTypeForPattern(arrayPattern); + if (!arrayType) { + return null; + } + const elementIndex = arrayPattern.elements.indexOf( + pattern as TSESTree.DestructuringPattern, + ); + if (elementIndex === -1) { + return null; + } + return getArrayElementType(arrayType, elementIndex); + } + return null; } diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 41be2ff0dc92..fb832260bdfc 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -249,5 +249,55 @@ ruleTester.run('no-useless-default-assignment', rule, { } `, }, + { + code: ` + declare const x: { hello: { world: string } }; + + const { + hello: { world = '' }, + } = x; + `, + errors: [ + { + column: 28, + data: { type: 'property' }, + endColumn: 30, + line: 5, + messageId: 'uselessDefaultAssignment', + }, + ], + output: ` + declare const x: { hello: { world: string } }; + + const { + hello: { world }, + } = x; + `, + }, + { + code: ` + declare const x: { hello: Array<{ world: string }> }; + + const { + hello: [{ world = '' }], + } = x; + `, + errors: [ + { + column: 29, + data: { type: 'property' }, + endColumn: 31, + line: 5, + messageId: 'uselessDefaultAssignment', + }, + ], + output: ` + declare const x: { hello: Array<{ world: string }> }; + + const { + hello: [{ world }], + } = x; + `, + }, ], }); From 79b3f398284baa5dfcd7d6a35c51b4603e3c7e33 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sat, 22 Nov 2025 14:10:45 +0100 Subject: [PATCH 26/35] add valid test case --- .../tests/rules/no-useless-default-assignment.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index fb832260bdfc..003cef921fea 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -117,6 +117,10 @@ ruleTester.run('no-useless-default-assignment', rule, { for (const [value = 'default'] of []) { } `, + ` + declare const x: [[number | undefined]]; + const [[a = 1]] = x; + `, ], invalid: [ { From 89c230f1234ae1bfd429628e62aa4378a22a638a Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sat, 22 Nov 2025 14:26:22 +0100 Subject: [PATCH 27/35] report contextual assignments and more test cases --- .../rules/no-useless-default-assignment.ts | 43 ++++++++----------- .../no-useless-default-assignment.test.ts | 40 +++++++++++++++++ 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index 7c30a337c0c7..f28c22d2cf3d 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -70,42 +70,35 @@ export default createRule<[], MessageId>({ return arrayType.getNumberIndexType() ?? null; } - function isCallbackFunction( - functionNode: - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionExpression, - ): boolean { - const parentType = functionNode.parent.type; - return ( - parentType !== AST_NODE_TYPES.MethodDefinition && - parentType !== AST_NODE_TYPES.VariableDeclarator && - parentType !== AST_NODE_TYPES.Property && - parentType !== AST_NODE_TYPES.ExpressionStatement && - parentType !== AST_NODE_TYPES.ReturnStatement - ); - } - function checkAssignmentPattern(node: TSESTree.AssignmentPattern): void { const parent = node.parent; - // Handle callback parameters (like array.map((a = 42) => ...)) if ( parent.type === AST_NODE_TYPES.ArrowFunctionExpression || parent.type === AST_NODE_TYPES.FunctionExpression ) { const paramIndex = parent.params.indexOf(node); if (paramIndex !== -1) { - if (!isCallbackFunction(parent)) { - return; - } - const tsFunc = services.esTreeNodeToTSNodeMap.get(parent); if (ts.isFunctionLike(tsFunc)) { - const signature = checker.getSignatureFromDeclaration(tsFunc); - if (signature) { - const params = signature.getParameters(); - if (paramIndex < params.length) { - const paramType = checker.getTypeOfSymbol(params[paramIndex]); + const contextualType = checker.getContextualType( + tsFunc as ts.Expression, + ); + if (!contextualType) { + return; + } + + const signatures = contextualType.getCallSignatures(); + if (signatures.length === 0) { + return; + } + + const signature = signatures[0]; + const params = signature.getParameters(); + if (paramIndex < params.length) { + const paramSymbol = params[paramIndex]; + if ((paramSymbol.flags & ts.SymbolFlags.Optional) === 0) { + const paramType = checker.getTypeOfSymbol(paramSymbol); if (!canBeUndefined(paramType)) { reportUselessDefault(node, 'parameter'); } diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 003cef921fea..43dabe4f4de3 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -121,6 +121,17 @@ ruleTester.run('no-useless-default-assignment', rule, { declare const x: [[number | undefined]]; const [[a = 1]] = x; `, + ` + function foo(x: string = '') {} + `, + ` + class C { + method(x: string = '') {} + } + `, + ` + const foo = (x: string = '') => {}; + `, ], invalid: [ { @@ -303,5 +314,34 @@ ruleTester.run('no-useless-default-assignment', rule, { } = x; `, }, + { + code: ` + interface B { + foo: (b: boolean | string) => void; + } + + const h: B = { + foo: (b = false) => {}, + }; + `, + errors: [ + { + column: 21, + data: { type: 'parameter' }, + endColumn: 26, + line: 7, + messageId: 'uselessDefaultAssignment', + }, + ], + output: ` + interface B { + foo: (b: boolean | string) => void; + } + + const h: B = { + foo: (b) => {}, + }; + `, + }, ], }); From 72cae2d0480a249c9d95fd57cb795966e538082f Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sun, 23 Nov 2025 18:56:22 +0100 Subject: [PATCH 28/35] report undefined as useless default assignment --- .../rules/no-useless-default-assignment.ts | 30 +++++-- .../no-useless-default-assignment.test.ts | 82 ++++++++++++++++--- 2 files changed, 97 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index f28c22d2cf3d..b8658c99cb4f 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -13,7 +13,7 @@ import { isTypeUnknownType, } from '../util'; -type MessageId = 'uselessDefaultAssignment'; +type MessageId = 'uselessDefaultAssignment' | 'uselessUndefined'; export default createRule<[], MessageId>({ name: 'no-useless-default-assignment', @@ -28,6 +28,8 @@ export default createRule<[], MessageId>({ messages: { uselessDefaultAssignment: 'Default value is useless because the {{ type }} is not optional.', + uselessUndefined: + 'Default value is useless because it is undefined. Optional {{ type }}s are already undefined by default.', }, schema: [], }, @@ -71,6 +73,19 @@ export default createRule<[], MessageId>({ } function checkAssignmentPattern(node: TSESTree.AssignmentPattern): void { + if ( + node.right.type === AST_NODE_TYPES.Identifier && + node.right.name === 'undefined' + ) { + const type = + node.parent.type === AST_NODE_TYPES.Property || + node.parent.type === AST_NODE_TYPES.ArrayPattern + ? 'property' + : 'parameter'; + reportUselessDefault(node, type, 'uselessUndefined'); + return; + } + const parent = node.parent; if ( @@ -100,7 +115,11 @@ export default createRule<[], MessageId>({ if ((paramSymbol.flags & ts.SymbolFlags.Optional) === 0) { const paramType = checker.getTypeOfSymbol(paramSymbol); if (!canBeUndefined(paramType)) { - reportUselessDefault(node, 'parameter'); + reportUselessDefault( + node, + 'parameter', + 'uselessDefaultAssignment', + ); } } } @@ -128,7 +147,7 @@ export default createRule<[], MessageId>({ } if (!canBeUndefined(propertyType)) { - reportUselessDefault(node, 'property'); + reportUselessDefault(node, 'property', 'uselessDefaultAssignment'); } } else if (parent.type === AST_NODE_TYPES.ArrayPattern) { const sourceType = getSourceTypeForPattern(parent); @@ -147,7 +166,7 @@ export default createRule<[], MessageId>({ } if (!canBeUndefined(elementType)) { - reportUselessDefault(node, 'property'); + reportUselessDefault(node, 'property', 'uselessDefaultAssignment'); } } } @@ -225,10 +244,11 @@ export default createRule<[], MessageId>({ function reportUselessDefault( node: TSESTree.AssignmentPattern, type: 'parameter' | 'property', + messageId: MessageId, ): void { context.report({ node: node.right, - messageId: 'uselessDefaultAssignment', + messageId, data: { type }, fix(fixer) { // Remove from before the = to the end of the default value diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 43dabe4f4de3..8f196deb937a 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -92,9 +92,7 @@ ruleTester.run('no-useless-default-assignment', rule, { ` function getValue(): undefined; function getValue(box: { value: string }): string; - function getValue({ value = undefined }: { value?: string } = {}): - | string - | undefined { + function getValue({ value = '' }: { value?: string } = {}): string | undefined { return value; } `, @@ -218,9 +216,7 @@ ruleTester.run('no-useless-default-assignment', rule, { code: ` function getValue(): undefined; function getValue(box: { value: string }): string; - function getValue({ value = undefined }: { value: string } = {}): - | string - | undefined { + function getValue({ value = '' }: { value: string } = {}): string | undefined { return value; } `, @@ -228,7 +224,7 @@ ruleTester.run('no-useless-default-assignment', rule, { { column: 37, data: { type: 'property' }, - endColumn: 46, + endColumn: 39, line: 4, messageId: 'uselessDefaultAssignment', }, @@ -236,9 +232,7 @@ ruleTester.run('no-useless-default-assignment', rule, { output: ` function getValue(): undefined; function getValue(box: { value: string }): string; - function getValue({ value }: { value: string } = {}): - | string - | undefined { + function getValue({ value }: { value: string } = {}): string | undefined { return value; } `, @@ -343,5 +337,73 @@ ruleTester.run('no-useless-default-assignment', rule, { }; `, }, + { + code: ` + function foo(a = undefined) {} + `, + errors: [ + { + column: 26, + data: { type: 'parameter' }, + endColumn: 35, + line: 2, + messageId: 'uselessUndefined', + }, + ], + output: ` + function foo(a) {} + `, + }, + { + code: ` + const { a = undefined } = {}; + `, + errors: [ + { + column: 21, + data: { type: 'property' }, + endColumn: 30, + line: 2, + messageId: 'uselessUndefined', + }, + ], + output: ` + const { a } = {}; + `, + }, + { + code: ` + const [a = undefined] = []; + `, + errors: [ + { + column: 20, + data: { type: 'property' }, + endColumn: 29, + line: 2, + messageId: 'uselessUndefined', + }, + ], + output: ` + const [a] = []; + `, + }, + { + code: ` + function foo({ a = undefined }) {} + `, + errors: [ + { + column: 28, + data: { type: 'property' }, + endColumn: 37, + line: 2, + messageId: 'uselessUndefined', + }, + ], + output: ` + function foo({ a }) {} + `, + }, ], }); From 39802f635b24ce5a2f00f8d38903e92871feae89 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Mon, 24 Nov 2025 20:04:43 +0100 Subject: [PATCH 29/35] add test cases, refactor and use nullThrows to increase test coverage --- .../rules/no-useless-default-assignment.ts | 70 +++++++------------ .../no-useless-default-assignment.test.ts | 14 ++++ 2 files changed, 41 insertions(+), 43 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index b8658c99cb4f..673c32c836ef 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -11,6 +11,8 @@ import { isTypeAnyType, isTypeFlagSet, isTypeUnknownType, + nullThrows, + NullThrowsReasons, } from '../util'; type MessageId = 'uselessDefaultAssignment' | 'uselessUndefined'; @@ -104,12 +106,7 @@ export default createRule<[], MessageId>({ } const signatures = contextualType.getCallSignatures(); - if (signatures.length === 0) { - return; - } - - const signature = signatures[0]; - const params = signature.getParameters(); + const params = signatures[0].getParameters(); if (paramIndex < params.length) { const paramSymbol = params[paramIndex]; if ((paramSymbol.flags & ts.SymbolFlags.Optional) === 0) { @@ -129,19 +126,7 @@ export default createRule<[], MessageId>({ } if (parent.type === AST_NODE_TYPES.Property) { - const objectPattern = parent.parent as TSESTree.ObjectPattern; - - const sourceType = getSourceTypeForPattern(objectPattern); - if (!sourceType) { - return; - } - - const propertyName = getPropertyName(parent.key); - if (!propertyName) { - return; - } - - const propertyType = getPropertyType(sourceType, propertyName); + const propertyType = getTypeOfProperty(parent); if (!propertyType) { return; } @@ -156,10 +141,6 @@ export default createRule<[], MessageId>({ } const elementIndex = parent.elements.indexOf(node); - if (elementIndex === -1) { - return; - } - const elementType = getArrayElementType(sourceType, elementIndex); if (!elementType) { return; @@ -171,12 +152,27 @@ export default createRule<[], MessageId>({ } } - function getSourceTypeForPattern(pattern: TSESTree.Node): ts.Type | null { - const parent = pattern.parent; - if (!parent) { + function getTypeOfProperty(node: TSESTree.Property): ts.Type | null { + const objectPattern = node.parent as TSESTree.ObjectPattern; + const sourceType = getSourceTypeForPattern(objectPattern); + if (!sourceType) { return null; } + const propertyName = getPropertyName(node.key); + if (!propertyName) { + return null; + } + + return getPropertyType(sourceType, propertyName); + } + + function getSourceTypeForPattern(pattern: TSESTree.Node): ts.Type | null { + const parent = nullThrows( + pattern.parent, + NullThrowsReasons.MissingParent, + ); + if (parent.type === AST_NODE_TYPES.VariableDeclarator && parent.init) { const tsNode = services.esTreeNodeToTSNodeMap.get(parent.init); return checker.getTypeAtLocation(tsNode); @@ -185,10 +181,10 @@ export default createRule<[], MessageId>({ if (isFunction(parent)) { const paramIndex = parent.params.indexOf(pattern as TSESTree.Parameter); const tsFunc = services.esTreeNodeToTSNodeMap.get(parent); - const signature = checker.getSignatureFromDeclaration(tsFunc); - if (!signature) { - return null; - } + const signature = nullThrows( + checker.getSignatureFromDeclaration(tsFunc), + NullThrowsReasons.MissingToken('signature', 'function'), + ); const params = signature.getParameters(); return checker.getTypeOfSymbol(params[paramIndex]); } @@ -198,16 +194,7 @@ export default createRule<[], MessageId>({ } if (parent.type === AST_NODE_TYPES.Property) { - const objectPattern = parent.parent as TSESTree.ObjectPattern; - const objectType = getSourceTypeForPattern(objectPattern); - if (!objectType) { - return null; - } - const propertyName = getPropertyName(parent.key); - if (!propertyName) { - return null; - } - return getPropertyType(objectType, propertyName); + return getTypeOfProperty(parent as TSESTree.Property); } if (parent.type === AST_NODE_TYPES.ArrayPattern) { @@ -219,9 +206,6 @@ export default createRule<[], MessageId>({ const elementIndex = arrayPattern.elements.indexOf( pattern as TSESTree.DestructuringPattern, ); - if (elementIndex === -1) { - return null; - } return getArrayElementType(arrayType, elementIndex); } diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 8f196deb937a..7595d9443bfb 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -130,6 +130,20 @@ ruleTester.run('no-useless-default-assignment', rule, { ` const foo = (x: string = '') => {}; `, + ` + const obj = { ab: { x: 1 } }; + const { + ['a' + 'b']: { x = 1 }, + } = obj; + `, + ` + const obj = { ab: 1 }; + const { ['a' + 'b']: x = 1 } = obj; + `, + ` + for ([[a = 1]] of []) { + } + `, ], invalid: [ { From 37ce8422e582802e0eafcfc03203b918628b47ce Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Wed, 26 Nov 2025 18:39:48 +0100 Subject: [PATCH 30/35] remove from eslint.config.mjs --- eslint.config.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index ae72b841b388..f4c3143af22e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -161,7 +161,6 @@ export default defineConfig( varsIgnorePattern: '^_', }, ], - '@typescript-eslint/no-useless-default-assignment': 'error', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/prefer-literal-enum-member': [ 'error', From 33c859ebe089ce9e931d8bb6a9659092572dcfc6 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Wed, 26 Nov 2025 18:43:56 +0100 Subject: [PATCH 31/35] make "when not to use it" more relevant to this rule --- .../eslint-plugin/docs/rules/no-useless-default-assignment.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx index b09fae97dd21..258e9b5b812b 100644 --- a/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx +++ b/packages/eslint-plugin/docs/rules/no-useless-default-assignment.mdx @@ -50,5 +50,4 @@ const [foo = ''] = [undefined]; ## When Not To Use It -If your codebase is still onboarding to TypeScript and/or has many existing `any`s or areas of loosely typed code, it may be difficult to enable this rule. -You might consider using ESLint disable comments for those specific situations instead of completely disabling this rule. +If you use default values defensively against runtime values that bypass type checking, or for documentation purposes, you may want to disable this rule. From 9334493d6d9c312376c8753380204fabf9f08862 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Mon, 1 Dec 2025 22:17:06 +0100 Subject: [PATCH 32/35] remove unnecessary type assertion --- .../eslint-plugin/src/rules/no-useless-default-assignment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index 673c32c836ef..cee360137e53 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -194,7 +194,7 @@ export default createRule<[], MessageId>({ } if (parent.type === AST_NODE_TYPES.Property) { - return getTypeOfProperty(parent as TSESTree.Property); + return getTypeOfProperty(parent); } if (parent.type === AST_NODE_TYPES.ArrayPattern) { From be81a7f9a8040e4220b9ffb5fd4bbba7dc3c33ed Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Mon, 1 Dec 2025 22:17:46 +0100 Subject: [PATCH 33/35] check for string index type --- .../rules/no-useless-default-assignment.ts | 2 +- .../no-useless-default-assignment.test.ts | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index cee360137e53..28c3f9497f79 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -55,7 +55,7 @@ export default createRule<[], MessageId>({ ): ts.Type | null { const symbol = objectType.getProperty(propertyName); if (!symbol) { - return null; + return objectType.getStringIndexType() ?? null; } return checker.getTypeOfSymbol(symbol); } diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 7595d9443bfb..1e82a833c33b 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -419,5 +419,43 @@ ruleTester.run('no-useless-default-assignment', rule, { function foo({ a }) {} `, }, + { + code: ` + declare const g: Record; + const { hello = '' } = g; + `, + errors: [ + { + column: 25, + data: { type: 'property' }, + endColumn: 27, + line: 3, + messageId: 'uselessDefaultAssignment', + }, + ], + output: ` + declare const g: Record; + const { hello } = g; + `, + }, + { + code: ` + declare const h: { [key: string]: string }; + const { world = '' } = h; + `, + errors: [ + { + column: 25, + data: { type: 'property' }, + endColumn: 27, + line: 3, + messageId: 'uselessDefaultAssignment', + }, + ], + output: ` + declare const h: { [key: string]: string }; + const { world } = h; + `, + }, ], }); From ed0011c2b9bb134abd2b5002f63d248ced0c094f Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Mon, 1 Dec 2025 22:17:59 +0100 Subject: [PATCH 34/35] support compiler option noUncheckedIndexedAccess properly --- .../rules/no-useless-default-assignment.ts | 9 +++++ .../no-useless-default-assignment.test.ts | 35 ++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index 28c3f9497f79..10a15c41ea3b 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -39,6 +39,11 @@ export default createRule<[], MessageId>({ create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); + const compilerOptions = services.program.getCompilerOptions(); + const isNoUncheckedIndexedAccess = tsutils.isCompilerOptionEnabled( + compilerOptions, + 'noUncheckedIndexedAccess', + ); function canBeUndefined(type: ts.Type): boolean { if (isTypeAnyType(type) || isTypeUnknownType(type)) { @@ -71,6 +76,10 @@ export default createRule<[], MessageId>({ } } + if (isNoUncheckedIndexedAccess) { + return null; + } + return arrayType.getNumberIndexType() ?? null; } diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 1e82a833c33b..9ecd0a60b4f5 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -1,6 +1,7 @@ import rule from '../../src/rules/no-useless-default-assignment'; -import { createRuleTesterWithTypes } from '../RuleTester'; +import { createRuleTesterWithTypes, getFixturesRootDir } from '../RuleTester'; +const rootDir = getFixturesRootDir(); const ruleTester = createRuleTesterWithTypes(); ruleTester.run('no-useless-default-assignment', rule, { @@ -144,6 +145,19 @@ ruleTester.run('no-useless-default-assignment', rule, { for ([[a = 1]] of []) { } `, + { + code: ` + declare const g: Array; + const [foo = ''] = g; + `, + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + projectService: false, + tsconfigRootDir: rootDir, + }, + }, + }, ], invalid: [ { @@ -457,5 +471,24 @@ ruleTester.run('no-useless-default-assignment', rule, { const { world } = h; `, }, + { + code: ` + declare const g: Array; + const [foo = ''] = g; + `, + errors: [ + { + column: 22, + data: { type: 'property' }, + endColumn: 24, + line: 3, + messageId: 'uselessDefaultAssignment', + }, + ], + output: ` + declare const g: Array; + const [foo] = g; + `, + }, ], }); From 026fd7f8129a27fc22b84bda38febe3b20b6c258 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Tue, 2 Dec 2025 22:29:10 +0100 Subject: [PATCH 35/35] also support noUncheckedIndexedAccess for records/mapped types --- .../rules/no-useless-default-assignment.ts | 3 +++ .../no-useless-default-assignment.test.ts | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts index 10a15c41ea3b..f33631df44a4 100644 --- a/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-useless-default-assignment.ts @@ -60,6 +60,9 @@ export default createRule<[], MessageId>({ ): ts.Type | null { const symbol = objectType.getProperty(propertyName); if (!symbol) { + if (isNoUncheckedIndexedAccess) { + return null; + } return objectType.getStringIndexType() ?? null; } return checker.getTypeOfSymbol(symbol); diff --git a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts index 9ecd0a60b4f5..d5d52b5bd0db 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-default-assignment.test.ts @@ -158,6 +158,32 @@ ruleTester.run('no-useless-default-assignment', rule, { }, }, }, + { + code: ` + declare const g: Record; + const { foo = '' } = g; + `, + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + projectService: false, + tsconfigRootDir: rootDir, + }, + }, + }, + { + code: ` + declare const h: { [key: string]: string }; + const { bar = '' } = h; + `, + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + projectService: false, + tsconfigRootDir: rootDir, + }, + }, + }, ], invalid: [ {