diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 134cefd76d1..3e1ad26bcda 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -24,6 +24,7 @@ import { isTypeAnyType, isTypeUnknownType, getTypeName, + getTypeOfPropertyOfName, } from '../util'; // Truthiness utilities @@ -499,7 +500,8 @@ export default createRule({ ); } if (propertyType.isNumberLiteral() || propertyType.isStringLiteral()) { - const propType = checker.getTypeOfPropertyOfType( + const propType = getTypeOfPropertyOfName( + checker, objType, propertyType.value.toString(), ); @@ -535,7 +537,11 @@ export default createRule({ const propertyType = getNodeType(node.property); return isNullablePropertyType(type, propertyType); } - const propType = checker.getTypeOfPropertyOfType(type, property.name); + const propType = getTypeOfPropertyOfName( + checker, + type, + property.name, + ); return propType && isNullableType(propType, { allowUndefined: true }); }); return ( diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 82828f13893..672f50dc4ff 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -6,6 +6,7 @@ export * from './isTypeReadonly'; export * from './misc'; export * from './nullThrows'; export * from './objectIterators'; +export * from './propertyTypes'; export * from './types'; // this is done for convenience - saves migrating all of the old rules diff --git a/packages/eslint-plugin/src/util/isTypeReadonly.ts b/packages/eslint-plugin/src/util/isTypeReadonly.ts index da9529e63a9..b10f00006ec 100644 --- a/packages/eslint-plugin/src/util/isTypeReadonly.ts +++ b/packages/eslint-plugin/src/util/isTypeReadonly.ts @@ -6,7 +6,7 @@ import { isPropertyReadonlyInType, } from 'tsutils'; import * as ts from 'typescript'; -import { nullThrows, NullThrowsReasons } from '.'; +import { getTypeOfPropertyOfType, nullThrows, NullThrowsReasons } from '.'; const enum Readonlyness { /** the type cannot be handled by the function */ @@ -101,7 +101,7 @@ function isTypeReadonlyObject( // doing this deep, potentially expensive check. for (const property of properties) { const propertyType = nullThrows( - checker.getTypeOfPropertyOfType(type, property.getName()), + getTypeOfPropertyOfType(checker, type, property), NullThrowsReasons.MissingToken(`property "${property.name}"`, 'type'), ); diff --git a/packages/eslint-plugin/src/util/propertyTypes.ts b/packages/eslint-plugin/src/util/propertyTypes.ts new file mode 100644 index 00000000000..5e2f1054239 --- /dev/null +++ b/packages/eslint-plugin/src/util/propertyTypes.ts @@ -0,0 +1,36 @@ +import * as ts from 'typescript'; + +export function getTypeOfPropertyOfName( + checker: ts.TypeChecker, + type: ts.Type, + name: string, + escapedName?: ts.__String, +): ts.Type | undefined { + // Most names are directly usable in the checker and aren't different from escaped names + if (!escapedName || !name.startsWith('__')) { + return checker.getTypeOfPropertyOfType(type, name); + } + + // Symbolic names may differ in their escaped name compared to their human-readable name + // https://github.com/typescript-eslint/typescript-eslint/issues/2143 + const escapedProperty = type + .getProperties() + .find(property => property.escapedName === escapedName); + + return escapedProperty + ? checker.getDeclaredTypeOfSymbol(escapedProperty) + : undefined; +} + +export function getTypeOfPropertyOfType( + checker: ts.TypeChecker, + type: ts.Type, + property: ts.Symbol, +): ts.Type | undefined { + return getTypeOfPropertyOfName( + checker, + type, + property.getName(), + property.getEscapedName(), + ); +} diff --git a/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts b/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts index 3ca6c0201fc..d5408a5d859 100644 --- a/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts @@ -238,6 +238,15 @@ ruleTester.run('prefer-readonly-parameter-types', rule, { } function foo(arg: Readonly) {} `, + ` + const sym = Symbol('sym'); + + interface WithSymbol { + [sym]: number; + } + + const willNotCrash = (foo: Readonly) => {}; + `, ], invalid: [ // arrays @@ -643,5 +652,24 @@ ruleTester.run('prefer-readonly-parameter-types', rule, { }, ], }, + { + code: ` + const sym = Symbol('sym'); + + interface WithSymbol { + [sym]: number; + } + + const willNot = (foo: WithSymbol) => {}; + `, + errors: [ + { + messageId: 'shouldBeReadonly', + line: 8, + column: 26, + endColumn: 41, + }, + ], + }, ], });