đź§© Creating a Custom ESLint Rule for Aligning TypeScript Interface Properties

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?

Leave a Comment

Your email address will not be published. Required fields are marked *