📄 consistent-indexed-object-style¶
📊 Analysis Summary¶
| Metric | Count |
|---|---|
| 🔧 Functions | 8 |
| 📦 Imports | 11 |
| 📑 Type Aliases | 2 |
📚 Table of Contents¶
🛠️ File Location:¶
📂 packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts
📤 Default Export¶
| Property | Value |
|---|---|
name |
'consistent-indexed-object-style' |
meta.type |
'suggestion' |
meta.docs.description |
'Require or disallow the Record type' |
meta.docs.recommended |
'stylistic' |
meta.fixable |
'code' |
meta.hasSuggestions |
true |
meta.messages.preferIndexSignature |
'An index signature is preferred over a record.' |
meta.messages.preferIndexSignatureSuggestion |
'Change into an index signature instead of a record.' |
meta.messages.preferRecord |
'A record is preferred over an index signature.' |
meta.messages.preferRecordSuggestion |
'Change into a record instead of an index signature.' |
meta.schema |
[ { type: 'string', description: 'Which indexed object syntax to prefer.', enum: ['record', 'index-signature'], }, ] |
defaultOptions |
['record'] |
Entry point: create — documented under Functions.
📦 Imports¶
| Name | Source |
|---|---|
ScopeVariable |
@typescript-eslint/scope-manager |
TSESLint |
@typescript-eslint/utils |
TSESTree |
@typescript-eslint/utils |
ReportFixFunction |
@typescript-eslint/utils/ts-eslint |
AST_NODE_TYPES |
@typescript-eslint/utils |
ASTUtils |
@typescript-eslint/utils |
createRule |
../util |
getFixOrSuggest |
../util |
isNodeEqual |
../util |
isParenthesized |
../util |
nullThrows |
../util |
Functions¶
create(context: any, [mode]: any): { TSInterfaceDeclaration(node: any): void; TSMappedType(nod…¶
Parameters:
contextany[mode]any
Returns: { TSInterfaceDeclaration(node: any): void; TSMappedType(node: any): void; TSTypeLiteral(node: any): void; TSTypeReference(node: any): void; }
Calls:
context.sourceCode .getCommentsInside(node) .somepreserved.everymember.parameters.atcontext.sourceCode.getScopeASTUtils.findVariableisDeeplyReferencingTypecontext.reportgetFixOrSuggest (from ../util)hasUnpreservedCommentscontext.sourceCode.getTextfixer.replaceTextnode.typeParameters.params .map(p => context.sourceCode.getText(p)) .joincheckMembersnullThrows (from ../util)scope.variables.findscopeManagerKey.references.someisParenthesized (from ../util)findParentDeclaration
Internal Comments:
// The fixers rebuild the type from the text of a few sub-nodes, so a
// comment inside `node` but outside all of those preserved sub-nodes would
// be dropped by the fix. Returns true when at least one such comment exists.
// If the key is used to compute the value, we can't convert to a Record.
// This is a weird special case, since modifiers are preserved by
// the mapped type, but not by the Record type. So this type is not,
// in general, equivalent to a Record type.
// If the mapped type is circular, we can't convert it to a Record. (x2)
// There's no builtin Mutable<T> type, so a `-readonly` mapped (x2)
// type can't be represented as a Record and is left untouched. (x2)
Code
create(context, [mode]) {
// The fixers rebuild the type from the text of a few sub-nodes, so a
// comment inside `node` but outside all of those preserved sub-nodes would
// be dropped by the fix. Returns true when at least one such comment exists.
function hasUnpreservedComments(
node: TSESTree.Node,
...preserved: (TSESTree.Node | undefined)[]
): boolean {
return context.sourceCode
.getCommentsInside(node)
.some(comment =>
preserved.every(
target =>
target == null ||
comment.range[0] < target.range[0] ||
comment.range[1] > target.range[1],
),
);
}
function checkMembers(
members: TSESTree.TypeElement[],
node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeLiteral,
parentId: TSESTree.Identifier | undefined,
prefix: string,
postfix: string,
safeFix = true,
): void {
if (members.length !== 1) {
return;
}
const [member] = members;
if (member.type !== AST_NODE_TYPES.TSIndexSignature) {
return;
}
const parameter = member.parameters.at(0);
if (parameter?.type !== AST_NODE_TYPES.Identifier) {
return;
}
const keyType = parameter.typeAnnotation;
if (!keyType) {
return;
}
const valueType = member.typeAnnotation;
if (!valueType) {
return;
}
if (parentId) {
const scope = context.sourceCode.getScope(parentId);
const superVar = ASTUtils.findVariable(scope, parentId.name);
if (
superVar &&
isDeeplyReferencingType(node, superVar, new Set([parentId]))
) {
return;
}
}
context.report({
node,
messageId: 'preferRecord',
...getFixOrSuggest({
fixOrSuggest: !safeFix
? 'none'
: hasUnpreservedComments(
node,
keyType.typeAnnotation,
valueType.typeAnnotation,
)
? 'suggest'
: 'fix',
suggestion: {
messageId: 'preferRecordSuggestion',
fix: (fixer): TSESLint.RuleFix => {
const key = context.sourceCode.getText(keyType.typeAnnotation);
const value = context.sourceCode.getText(
valueType.typeAnnotation,
);
const record = member.readonly
? `Readonly<Record<${key}, ${value}>>`
: `Record<${key}, ${value}>`;
return fixer.replaceText(node, `${prefix}${record}${postfix}`);
},
},
}),
});
}
return {
...(mode === 'index-signature' && {
TSTypeReference(node): void {
const typeName = node.typeName;
if (typeName.type !== AST_NODE_TYPES.Identifier) {
return;
}
if (typeName.name !== 'Record') {
return;
}
const params = node.typeArguments?.params;
if (params?.length !== 2) {
return;
}
const indexParam = params[0];
const shouldFix =
indexParam.type === AST_NODE_TYPES.TSStringKeyword ||
indexParam.type === AST_NODE_TYPES.TSNumberKeyword ||
indexParam.type === AST_NODE_TYPES.TSSymbolKeyword;
context.report({
node,
messageId: 'preferIndexSignature',
...getFixOrSuggest({
fixOrSuggest:
shouldFix && !hasUnpreservedComments(node, params[0], params[1])
? 'fix'
: 'suggest',
suggestion: {
messageId: 'preferIndexSignatureSuggestion',
fix: fixer => {
const key = context.sourceCode.getText(params[0]);
const type = context.sourceCode.getText(params[1]);
return fixer.replaceText(node, `{ [key: ${key}]: ${type} }`);
},
},
}),
});
},
}),
...(mode === 'record' && {
TSInterfaceDeclaration(node): void {
let genericTypes = '';
if (node.typeParameters?.params.length) {
genericTypes = `<${node.typeParameters.params
.map(p => context.sourceCode.getText(p))
.join(', ')}>`;
}
checkMembers(
node.body.body,
node,
node.id,
`type ${node.id.name}${genericTypes} = `,
';',
!node.extends.length &&
node.parent.type !== AST_NODE_TYPES.ExportDefaultDeclaration,
);
},
TSMappedType(node): void {
const key = node.key;
const scope = context.sourceCode.getScope(key);
const scopeManagerKey = nullThrows(
scope.variables.find(
value => value.name === key.name && value.isTypeVariable,
),
'key type parameter must be a defined type variable in its scope',
);
// If the key is used to compute the value, we can't convert to a Record.
if (
scopeManagerKey.references.some(
reference => reference.isTypeReference,
)
) {
return;
}
const constraint = node.constraint;
if (
constraint.type === AST_NODE_TYPES.TSTypeOperator &&
constraint.operator === 'keyof' &&
!isParenthesized(constraint, context.sourceCode)
) {
// This is a weird special case, since modifiers are preserved by
// the mapped type, but not by the Record type. So this type is not,
// in general, equivalent to a Record type.
return;
}
// If the mapped type is circular, we can't convert it to a Record.
const parentId = findParentDeclaration(node)?.id;
if (parentId) {
const scope = context.sourceCode.getScope(key);
const superVar = ASTUtils.findVariable(scope, parentId.name);
if (superVar) {
const isCircular = isDeeplyReferencingType(
node.parent,
superVar,
new Set([parentId]),
);
if (isCircular) {
return;
}
}
}
context.report({
node,
messageId: 'preferRecord',
...getFixOrSuggest({
// There's no builtin Mutable<T> type, so a `-readonly` mapped
// type can't be represented as a Record and is left untouched.
fixOrSuggest:
node.readonly === '-'
? 'none'
: hasUnpreservedComments(
node,
constraint,
node.typeAnnotation,
)
? 'suggest'
: 'fix',
suggestion: {
messageId: 'preferRecordSuggestion',
fix: (fixer): ReturnType<ReportFixFunction> => {
const keyType = context.sourceCode.getText(constraint);
const valueType = node.typeAnnotation
? context.sourceCode.getText(node.typeAnnotation)
: 'any';
let recordText = `Record<${keyType}, ${valueType}>`;
if (node.optional === '+' || node.optional === true) {
recordText = `Partial<${recordText}>`;
} else if (node.optional === '-') {
recordText = `Required<${recordText}>`;
}
if (node.readonly === '+' || node.readonly === true) {
recordText = `Readonly<${recordText}>`;
}
return fixer.replaceText(node, recordText);
},
},
}),
});
},
TSTypeLiteral(node): void {
const parent = findParentDeclaration(node);
checkMembers(node.members, node, parent?.id, '', '');
},
}),
};
}
findParentDeclaration(node: TSESTree.Node): TSESTree.TSTypeAliasDeclaration | undefined¶
Parameters:
nodeTSESTree.Node
Returns: TSESTree.TSTypeAliasDeclaration | undefined
Calls:
findParentDeclaration
Code
function findParentDeclaration(
node: TSESTree.Node,
): TSESTree.TSTypeAliasDeclaration | undefined {
if (node.parent && node.parent.type !== AST_NODE_TYPES.TSTypeAnnotation) {
if (node.parent.type === AST_NODE_TYPES.TSTypeAliasDeclaration) {
return node.parent;
}
return findParentDeclaration(node.parent);
}
return undefined;
}
isDeeplyReferencingType(node: TSESTree.Node, superVar: ScopeVariable, visited: Set<TSESTree.Node>): boolean¶
Parameters:
nodeTSESTree.NodesuperVarScopeVariablevisitedSet<TSESTree.Node>
Returns: boolean
Calls:
visited.hasvisited.addnode.members.someisDeeplyReferencingType[node.indexType, node.objectType].some[ node.checkType, node.extendsType, node.falseType, node.trueType, ].somenode.types.somenode.body.body.somenode.params.somesuperVar.references.someisNodeEqual (from ../util)ASTUtils.findVariablerefVar.defs.some
Internal Comments:
// something on the chain is circular but it's not the reference being checked
// check if the identifier is a reference of the type being checked
// otherwise, follow its definition(s) (x2)
Code
function isDeeplyReferencingType(
node: TSESTree.Node,
superVar: ScopeVariable,
visited: Set<TSESTree.Node>,
): boolean {
if (visited.has(node)) {
// something on the chain is circular but it's not the reference being checked
return false;
}
visited.add(node);
switch (node.type) {
case AST_NODE_TYPES.TSTypeLiteral:
return node.members.some(member =>
isDeeplyReferencingType(member, superVar, visited),
);
case AST_NODE_TYPES.TSTypeAliasDeclaration:
return isDeeplyReferencingType(node.typeAnnotation, superVar, visited);
case AST_NODE_TYPES.TSIndexedAccessType:
return [node.indexType, node.objectType].some(type =>
isDeeplyReferencingType(type, superVar, visited),
);
case AST_NODE_TYPES.TSMappedType:
if (node.typeAnnotation) {
return isDeeplyReferencingType(node.typeAnnotation, superVar, visited);
}
break;
case AST_NODE_TYPES.TSConditionalType:
return [
node.checkType,
node.extendsType,
node.falseType,
node.trueType,
].some(type => isDeeplyReferencingType(type, superVar, visited));
case AST_NODE_TYPES.TSUnionType:
case AST_NODE_TYPES.TSIntersectionType:
return node.types.some(type =>
isDeeplyReferencingType(type, superVar, visited),
);
case AST_NODE_TYPES.TSInterfaceDeclaration:
return node.body.body.some(type =>
isDeeplyReferencingType(type, superVar, visited),
);
case AST_NODE_TYPES.TSTypeAnnotation:
return isDeeplyReferencingType(node.typeAnnotation, superVar, visited);
case AST_NODE_TYPES.TSIndexSignature: {
if (node.typeAnnotation) {
return isDeeplyReferencingType(node.typeAnnotation, superVar, visited);
}
break;
}
case AST_NODE_TYPES.TSTypeParameterInstantiation: {
return node.params.some(param =>
isDeeplyReferencingType(param, superVar, visited),
);
}
case AST_NODE_TYPES.TSTypeReference: {
if (isDeeplyReferencingType(node.typeName, superVar, visited)) {
return true;
}
if (
node.typeArguments &&
isDeeplyReferencingType(node.typeArguments, superVar, visited)
) {
return true;
}
break;
}
case AST_NODE_TYPES.Identifier: {
// check if the identifier is a reference of the type being checked
if (superVar.references.some(ref => isNodeEqual(ref.identifier, node))) {
return true;
}
// otherwise, follow its definition(s)
const refVar = ASTUtils.findVariable(superVar.scope, node.name);
if (refVar) {
return refVar.defs.some(def =>
isDeeplyReferencingType(def.node, superVar, visited),
);
}
}
}
return false;
}
Internal helpers¶
Declared inside another function in this file.
hasUnpreservedComments(node: TSESTree.Node, preserved: (TSESTree.Node | undefined)[]): boolean¶
Parameters:
nodeTSESTree.Nodepreserved(TSESTree.Node | undefined)[]
Returns: boolean
Calls:
context.sourceCode .getCommentsInside(node) .somepreserved.every
Code
function hasUnpreservedComments(
node: TSESTree.Node,
...preserved: (TSESTree.Node | undefined)[]
): boolean {
return context.sourceCode
.getCommentsInside(node)
.some(comment =>
preserved.every(
target =>
target == null ||
comment.range[0] < target.range[0] ||
comment.range[1] > target.range[1],
),
);
}
checkMembers(…): void¶
Parameters:
membersTSESTree.TypeElement[]nodeTSESTree.TSInterfaceDeclaration | TSESTree.TSTypeLiteralparentIdTSESTree.Identifier | undefinedprefixstringpostfixstringsafeFixboolean
Returns: void
Calls:
member.parameters.atcontext.sourceCode.getScopeASTUtils.findVariableisDeeplyReferencingTypecontext.reportgetFixOrSuggest (from ../util)hasUnpreservedCommentscontext.sourceCode.getTextfixer.replaceText
Code
function checkMembers(
members: TSESTree.TypeElement[],
node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeLiteral,
parentId: TSESTree.Identifier | undefined,
prefix: string,
postfix: string,
safeFix = true,
): void {
if (members.length !== 1) {
return;
}
const [member] = members;
if (member.type !== AST_NODE_TYPES.TSIndexSignature) {
return;
}
const parameter = member.parameters.at(0);
if (parameter?.type !== AST_NODE_TYPES.Identifier) {
return;
}
const keyType = parameter.typeAnnotation;
if (!keyType) {
return;
}
const valueType = member.typeAnnotation;
if (!valueType) {
return;
}
if (parentId) {
const scope = context.sourceCode.getScope(parentId);
const superVar = ASTUtils.findVariable(scope, parentId.name);
if (
superVar &&
isDeeplyReferencingType(node, superVar, new Set([parentId]))
) {
return;
}
}
context.report({
node,
messageId: 'preferRecord',
...getFixOrSuggest({
fixOrSuggest: !safeFix
? 'none'
: hasUnpreservedComments(
node,
keyType.typeAnnotation,
valueType.typeAnnotation,
)
? 'suggest'
: 'fix',
suggestion: {
messageId: 'preferRecordSuggestion',
fix: (fixer): TSESLint.RuleFix => {
const key = context.sourceCode.getText(keyType.typeAnnotation);
const value = context.sourceCode.getText(
valueType.typeAnnotation,
);
const record = member.readonly
? `Readonly<Record<${key}, ${value}>>`
: `Record<${key}, ${value}>`;
return fixer.replaceText(node, `${prefix}${record}${postfix}`);
},
},
}),
});
}
suggestion.fix(fixer: any): TSESLint.RuleFix¶
Parameters:
fixerany
Returns: TSESLint.RuleFix
Calls:
context.sourceCode.getTextfixer.replaceText
Code
(fixer): TSESLint.RuleFix => {
const key = context.sourceCode.getText(keyType.typeAnnotation);
const value = context.sourceCode.getText(
valueType.typeAnnotation,
);
const record = member.readonly
? `Readonly<Record<${key}, ${value}>>`
: `Record<${key}, ${value}>`;
return fixer.replaceText(node, `${prefix}${record}${postfix}`);
}
suggestion.fix(fixer: any): any¶
Parameters:
fixerany
Returns: any
Calls:
context.sourceCode.getTextfixer.replaceText
Code
suggestion.fix(fixer: any): ReturnType<ReportFixFunction>¶
Parameters:
fixerany
Returns: ReturnType<ReportFixFunction>
Calls:
context.sourceCode.getTextfixer.replaceText
Code
(fixer): ReturnType<ReportFixFunction> => {
const keyType = context.sourceCode.getText(constraint);
const valueType = node.typeAnnotation
? context.sourceCode.getText(node.typeAnnotation)
: 'any';
let recordText = `Record<${keyType}, ${valueType}>`;
if (node.optional === '+' || node.optional === true) {
recordText = `Partial<${recordText}>`;
} else if (node.optional === '-') {
recordText = `Required<${recordText}>`;
}
if (node.readonly === '+' || node.readonly === true) {
recordText = `Readonly<${recordText}>`;
}
return fixer.replaceText(node, recordText);
}
Type Aliases¶
MessageIds¶
type MessageIds = | 'preferIndexSignature'
| 'preferIndexSignatureSuggestion'
| 'preferRecord'
| 'preferRecordSuggestion';
Options¶
Generated by Syntax Scribe