More Lint Rules
August 19, 2020 • 5 minute read
Last week, I created two more ESLint rules.
The first is an ESLint-port of a TypeScript-specific Wotan rule — Wotan is yet another linter — named no-misused-generics
. It’s a rule that Oliver Ash mentioned in a conversation that stemmed a tweet of Dan Vanderkam’s:
The Golden Rule of Generics in @typescript (probably other languages, too!) h/t to @SeaRyanC and the new TS handbook for this amazingly concrete rule. https://t.co/Z3ETr1MQ22
— Dan Vanderkam (@danvdk) August 12, 2020
You should read Dan’s blog post. It explains the reasoning behind the rule.
The port was simple, as the Wotan rule’s implementation has a single top-level function. All that had to be done was to call that function whenever ESLint encounters an AST node that has a call TypeScript signature:
return {
ArrowFunctionExpression: checkSignature,
FunctionDeclaration: checkSignature,
FunctionExpression: checkSignature,
MethodDefinition: checkSignature,
"Program:exit": () => (usage = undefined),
TSCallSignatureDeclaration: checkSignature,
TSConstructorType: checkSignature,
TSConstructSignatureDeclaration: checkSignature,
TSDeclareFunction: checkSignature,
TSFunctionType: checkSignature,
TSIndexSignature: checkSignature,
TSMethodSignature: checkSignature,
TSPropertySignature: checkSignature,
};
and then obtain the corresponding TypeScript AST node via the esTreeNodeToTSNodeMap
map that’s provided by the @typescript-eslint/parser
:
const { esTreeNodeToTSNodeMap } = getParserServices(context);let usage: Map<ts.Identifier, tsutils.VariableInfo> | undefined;
function checkSignature(node: es.Node) {
const tsNode = esTreeNodeToTSNodeMap.get(node); if (
tsutils.isSignatureDeclaration(tsNode) &&
tsNode.typeParameters !== undefined
) {
checkTypeParameters(tsNode.typeParameters, tsNode);
}
}
With the rule enabled, misused generics like this:
function parseYAML<T>(input: string): T {
/* parsing implementation */
}
will effect a lint failure:
Type parameter 'T' cannot be inferred from any parameter.
Here, the generic is performing the role of a type assertion. There is no guarantee that the runtime return value will be whatever is specified for T
. It could be anything.
Instead the function should specify unknown
as the return type, requiring the caller to use an explicit type assertion — or, better still, a user-defined type guard.
There are exceptions to the rule — for example, some RxJS operators would fail, as they infer a type parameter from the source observable upon which pipe
is called. However, exceptions are relatively rare and my recommendation would be to use the rule as a warning — or as an error with local overrides.
You can find the rule in eslint-plugin-etc
. It has same name as the Wotan rule: no-misused-generics
.
The second rule has a related tweet, too. It’s this one, from Sophie Alpert:
WTB a lint rule that detects
— Sophie Alpert (@sophiebits) August 13, 2020
const [processedData, setProcessedData] = useState();
useEffect(() => {
let processed = /* do something with data */;
setProcessedData(processed);
}, [data]);
and tells you to use useMemo instead.
The problem with the code in the tweet’s snippet is that it effects an additional render and reconciliation. The function passed to useEffect
runs after a render is committed and the setState
call made within the function effects another render.
Wherever useEffect
is used to memoize the synchronous processing of data, it can be replaced with useMemo
and the additional render and reconciliation can be avoided, like this:
const processedData = useMemo(() => {
let processed = /* do something with data */;
return processed;
}, [data]);
The lint rule that I wrote — prefer-usememo
in eslint-plugin-react-etc
— uses a simple heuristic to determine whether or not a useEffect
call can be replaced with useMemo
.
The rule will suggest useMemo
whenever the function passed to useEffect
:
- makes an single, unconditional, clearly-synchronous call to a
useState
setter; - does not reference or call additional
useState
setters; - has some dependencies; and
- does not return a teardown function.
The heuristic identifies the useEffect
usage in Sophie’s tweet — and suggests useMemo
as a replacement — and it hasn’t effected any false positives in the code bases over which I’ve run the rule. The rule’s tests include some of the use cases for which the rule will not suggest useMemo
as a replacement.
The rule is implemented in TypeScript — there are other React lint rules that I’ll be adding to eslint-plugin-react-etc
and they will take advantage of information from the type system — but it doesn’t rely upon TypeScript-specific AST nodes, so it will work just fine with ESLint configurations that do not use the TypeScript parser.