
In large-scale TypeScript projects, code consistency is more than just a nice-to-have—it’s a necessity for clean, maintainable code. One small but impactful area is how interface properties are aligned.
When interfaces have misaligned colons (:
), it may seem trivial—but over time, those little inconsistencies add up, making your code harder to scan, debug, or review. So why not automate it?
In this post, I’ll walk you through creating a custom ESLint rule that enforces consistent alignment for interface properties—along with an auto-fix feature to clean things up automatically.
🎯 What Our ESLint Rule Will Do
We’re calling this rule: interface-properties-alignment
.
It will:
- Enforce that all
:
in interface properties line up vertically. - Support an optional padding value to control spacing between the property name and the colon.
âś… Sample Configuration
"@nx/workspace-interface-properties-alignment": [
"warn",
{ "padding": 2 }
]
🛠️ Implementation Overview
1. Define Rule Metadata
This is the rule’s declaration, including its type (layout
), schema, and fixable behavior.
export const rule = ESLintUtils.RuleCreator(() => __filename)<Options, 'ErrorMessage'>({
name: 'interface-properties-alignment',
meta: {
type: 'layout',
docs: {
description: 'Ensure interface properties are aligned properly.',
url: '', // optional documentation link
},
schema: [
{
type: 'object',
properties: {
padding: { type: 'number' },
},
additionalProperties: false,
},
],
fixable: 'whitespace',
messages: {
ErrorMessage: 'Interface properties should be aligned',
},
},
defaultOptions: [{ padding: 0 }],
create(context, [{ padding }]) {
// Main logic in the next step
},
});
2. Main Rule Logic
The rule targets TSInterfaceDeclaration
nodes. It analyzes the positions of each property’s colon (:
) and checks if they’re all aligned, based on the longest property name and configured padding.
Here’s a stripped-down version of the core logic:
InterfaceDeclaration(node) {
const properties = node.body.body.filter(p => p.type === 'TSPropertySignature');
const colonPositions = properties.map(prop =>
context.sourceCode.getTokens(prop.typeAnnotation!)
.find(t => t.type === AST_TOKEN_TYPES.Punctuator)?.loc.start.column || 0
);
const identifierEnds = properties.map(prop =>
prop.key.loc.end.column + (prop.optional || prop.computed ? 1 : 0)
);
const maxColon = Math.max(...colonPositions);
const maxIdentifierEnd = Math.max(...identifierEnds);
const isMisaligned = !colonPositions.every(pos => pos === maxColon);
const incorrectPadding = maxIdentifierEnd + padding !== maxColon;
if (isMisaligned || incorrectPadding) {
context.report({
node,
messageId: 'ErrorMessage',
fix: (fixer) =>
properties.map((prop) => {
const colonToken = context.sourceCode.getFirstToken(prop.typeAnnotation!);
const extra = prop.optional || prop.computed ? 1 : 0;
const space = maxIdentifierEnd - (prop.key.loc.end.column + extra - padding);
const range: TSESTree.Range = [prop.key.range[1] + extra, colonToken?.range[0] ?? 0];
return fixer.replaceTextRange(range, ''.padEnd(space, ' '));
}),
});
}
}
đź”§ The Auto-Fixer
The fixer adjusts the spacing between the property name and the colon so everything aligns based on the longest property + configured padding.
đź’» Example Usage
đź”´ Before (Input)
interface Example {
name : string;
age : number;
location : string;
}
âś… After (Output)
interface Example {
name : string;
age : number;
location : string;
}
đź§Ş Testing the Rule
We’ll use RuleTester
to validate both correct and incorrect usage:
const ruleTester = new ESLintUtils.RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
});
ruleTester.run('interface-properties-alignment', rule, {
valid: [
{
code: `
interface Test {
propOne: string;
propTwo: number;
}
`,
options: [{ padding: 0 }],
},
],
invalid: [
{
code: `
interface Test {
propOne : string;
propTwo: number;
}
`,
options: [{ padding: 0 }],
errors: [{ messageId: 'ErrorMessage' }],
output: `
interface Test {
propOne : string;
propTwo : number;
}
`,
},
],
});
✨ Final Thoughts
This custom ESLint rule might seem like a small enhancement, but it saves time, reduces review comments, and keeps your TypeScript interfaces clean and aligned.
If you’re working in a large codebase or on a team, it’s these little consistency boosters that make a big difference long-term.
🙋‍♂️ Want help turning this into a reusable ESLint plugin or integrate it into a monorepo with Nx? Let me know—I’d be happy to walk through that next!
Would you like me to generate a VS Code extension for formatting this automatically on save too?