Skip to content

Instantly share code, notes, and snippets.

@webstrand
Last active April 14, 2025 18:01
Show Gist options
  • Save webstrand/8f48d5de392be14e74608864e768235e to your computer and use it in GitHub Desktop.
Save webstrand/8f48d5de392be14e74608864e768235e to your computer and use it in GitHub Desktop.
// 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