I can only offer you a solution for typescript.
I believe that this cannot be achieved only with TSLint / ESLint.
There is a so-called class-name rule that can partially solve the problem, but it seems you need to write your own rule for this case.
So, try to write such a tslint rule . To do this, we need to use the rulesDirectory option in the tslint configuration to specify the path to user rules
"rulesDirectory": [ "./tools/tslint-rules/" ],
Since I'm going to write my own rule in typescript, I will use one function added in tslint@5.7.0
[improvement] custom lint rules will be allowed using the node permission path for loaders like ts-node (# 3108)
We need to install the ts-node package
npm i -D ts-node
Then add the fake rule to tslint.json
"ts-loader": true,
and create the tsLoaderRule.js file in our tsLoaderRule.js rules:
const path = require('path'); const Lint = require('tslint'); // Custom rule that registers all of the custom rules, written in TypeScript, with ts-node. // This is necessary, because `tslint` and IDEs won't execute any rules that aren't in a .js file. require('ts-node').register({ project: path.join(__dirname, '../tsconfig.json') }); // Add a noop rule so tslint doesn't complain. exports.Rule = class Rule extends Lint.Rules.AbstractRule { apply() {} };
This is basically an approach that is widely used in angular packages such as angular, universal, etc.
Now we can create our custom rule (an extended version of the class-name rule), which will be written in typescript.
myReactComponentRule.ts
import * as ts from 'typescript'; import * as Lint from 'tslint'; export class Rule extends Lint.Rules.AbstractRule { static metadata: Lint.IRuleMetadata = { ruleName: 'my-react-component', description: 'Enforces PascalCased React component class.', rationale: 'Makes it easy to differentiate classes from regular variables at a glance.', optionsDescription: 'Not configurable.', options: null, optionExamples: [true], type: 'style', typescriptOnly: false, }; static FAILURE_STRING = (className: string) => `React component ${className} must be PascalCased and prefixed by Component`; static validate(name: string): boolean { return isUpperCase(name[0]) && !name.includes('_') && name.endsWith('Component'); } apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { return this.applyWithFunction(sourceFile, walk); } } function walk(ctx: Lint.WalkContext<void>) { return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void { if (isClassLikeDeclaration(node) && node.name !== undefined && isReactComponent(node)) { if (!Rule.validate(node.name!.text)) { ctx.addFailureAtNode(node.name!, Rule.FAILURE_STRING(node.name!.text)); } } return ts.forEachChild(node, cb); }); } function isClassLikeDeclaration(node: ts.Node): node is ts.ClassLikeDeclaration { return node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.ClassExpression; } function isReactComponent(node: ts.Node): boolean { let result = false; const classDeclaration = <ts.ClassDeclaration> node; if (classDeclaration.heritageClauses) { classDeclaration.heritageClauses.forEach((hc) => { if (hc.token === ts.SyntaxKind.ExtendsKeyword && hc.types) { hc.types.forEach(type => { if (type.getText() === 'React.Component') { result = true; } }); } }); } return result; } function isUpperCase(str: string): boolean { return str === str.toUpperCase(); }
and finally, we have to put our new rule in tsling.json :
// Custom rules "ts-loader": true, "my-react-component": true
So code like
App extends React.Component
will result in:

I also created an exempt ts response application where you can try it.
Update
I think grandparents tracking class names won't be a trivial task
Indeed, we can handle inheritance. To do this, we need to create a rule extended from the Lint.Rules.TypedRule class in order to have access to TypeChecker :
myReactComponentRule.ts
import * as ts from 'typescript'; import * as Lint from 'tslint'; export class Rule extends Lint.Rules.TypedRule { static metadata: Lint.IRuleMetadata = { ruleName: 'my-react-component', description: 'Enforces PascalCased React component class.', rationale: 'Makes it easy to differentiate classes from regular variables at a glance.', optionsDescription: 'Not configurable.', options: null, optionExamples: [true], type: 'style', typescriptOnly: false, }; static FAILURE_STRING = (className: string) => `React component ${className} must be PascalCased and prefixed by Component`; static validate(name: string): boolean { return isUpperCase(name[0]) && !name.includes('_') && name.endsWith('Component'); } applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { return this.applyWithFunction(sourceFile, walk, undefined, program.getTypeChecker()); } } function walk(ctx: Lint.WalkContext<void>, tc: ts.TypeChecker) { return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void { if ( isClassLikeDeclaration(node) && node.name !== undefined && containsType(tc.getTypeAtLocation(node), isReactComponentType) && !Rule.validate(node.name!.text)) { ctx.addFailureAtNode(node.name!, Rule.FAILURE_STRING(node.name!.text)); } return ts.forEachChild(node, cb); }); } function containsType(type: ts.Type, predicate: (symbol: any) => boolean): boolean { if (type.symbol !== undefined && predicate(type.symbol)) { return true; } const bases = type.getBaseTypes(); return bases && bases.some((t) => containsType(t, predicate)); } function isReactComponentType(symbol: any) { return symbol.name === 'Component' && symbol.parent && symbol.parent.name === 'React'; } function isClassLikeDeclaration(node: ts.Node): node is ts.ClassLikeDeclaration { return node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.ClassExpression; } function isUpperCase(str: string): boolean { return str === str.toUpperCase(); }
See also commit: