Last active
April 14, 2025 18:01
-
-
Save webstrand/8f48d5de392be14e74608864e768235e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MIT License 2024 Webstrand | |
import tseslint from "typescript-eslint"; | |
import {RuleListener, RuleModule} from "@typescript-eslint/utils/ts-eslint"; | |
export default tseslint.config( | |
tseslint.configs.strictTypeCheckedOnly, // no idea, the files don't match if this is missing | |
{ | |
plugins: { | |
webstrand: { | |
rules: { | |
"no-narrower-assignment-than-type": (< | |
T extends RuleModule<"narrowerAssignment">, | |
>( | |
x: T, | |
) => x)({ | |
create(context) { | |
const parserServices = | |
context.sourceCode.parserServices; | |
if ( | |
!parserServices || | |
!parserServices.program || | |
!parserServices.esTreeNodeToTSNodeMap | |
) | |
return {}; | |
const checker = | |
parserServices.program.getTypeChecker(); | |
return { | |
VariableDeclarator(node) { | |
const parent = node.parent; | |
// Visit only const VariableDeclarators | |
if (!parent || parent.kind !== "const") | |
return; | |
// Make sure we have both an ID with a type annotation and an init value | |
if ( | |
!node.id || | |
!node.id.typeAnnotation || | |
!node.init | |
) | |
return; | |
const idTsNode = | |
parserServices.esTreeNodeToTSNodeMap!.get( | |
node.id, | |
); | |
const initTsNode = | |
parserServices.esTreeNodeToTSNodeMap!.get( | |
node.init, | |
); | |
if (!idTsNode || !initTsNode) return; | |
const declaredType = | |
checker.getTypeAtLocation(idTsNode); | |
const initType = | |
checker.getBaseTypeOfLiteralType( | |
checker.getTypeAtLocation( | |
initTsNode, | |
), | |
); | |
const initAssignableToDeclared = | |
checker.isTypeAssignableTo( | |
initType, | |
declaredType, | |
); | |
const declaredAssignableToInit = | |
checker.isTypeAssignableTo( | |
declaredType, | |
initType, | |
); | |
if ( | |
initAssignableToDeclared && | |
!declaredAssignableToInit | |
) { | |
context.report({ | |
node, | |
messageId: "narrowerAssignment", | |
data: { | |
declaredType: | |
checker.typeToString( | |
declaredType, | |
), | |
initType: | |
checker.typeToString( | |
initType, | |
), | |
}, | |
}); | |
} | |
}, | |
} satisfies RuleListener; | |
}, | |
defaultOptions: [], | |
meta: { | |
docs: { | |
description: | |
"Prevent const variables from having assignments narrower than their declared type", | |
recommended: "error", | |
}, | |
messages: { | |
narrowerAssignment: | |
"Variable is declared with type '{{declaredType}}' but initialized with a narrower type '{{initType}}'. Consider using the narrower type in the declaration or using a more specific value.", | |
}, | |
schema: [], | |
type: "suggestion", | |
requiresTypeChecking: true, | |
}, | |
}), | |
}, | |
}, | |
}, | |
rules: { | |
"webstrand/no-narrower-assignment-than-type": "error", | |
}, | |
}, | |
{ | |
languageOptions: { | |
parserOptions: { | |
projectService: true, | |
tsconfigRootDir: import.meta.dirname, | |
}, | |
}, | |
}, | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment