Skip to content

Commit c69f277

Browse files
committed
feat: add groups option in sort-union-types rule
1 parent c4977df commit c69f277

File tree

3 files changed

+591
-8
lines changed

3 files changed

+591
-8
lines changed

docs/content/rules/sort-union-types.mdx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,64 @@ Controls whether sorting should be case-sensitive or not.
122122
- `true` — Ignore case when sorting alphabetically or naturally (e.g., “A” and “a” are the same).
123123
- `false` — Consider case when sorting (e.g., “A” comes before “a”).
124124

125+
### groups
126+
127+
<sub>(default: `[]`)</sub>
128+
129+
Allows you to specify a list of union type groups for sorting. Groups help organize types into categories, making your type definitions more readable and maintainable. Multiple groups can be combined to achieve the desired sorting order.
130+
131+
There are a lot of predefined groups.
132+
133+
Predefined Groups:
134+
135+
- `'conditional`' — Conditional types.
136+
- `'function`' — Function types.
137+
- `'import`' — Imported types.
138+
- `'intersection`' — Intersection types.
139+
- `'keyword`' — Keyword types.
140+
- `'literal`' — Literal types.
141+
- `'named`' — Named types.
142+
- `'object`' — Object types.
143+
- `'operator`' — Operator types.
144+
- `'tuple`' — Tuple types.
145+
- `'union`' — Union types.
146+
- `'nullish`' — Nullish types (`null` or `undefined`).
147+
- `'unknown`' — Types that don’t fit into any other group.
148+
149+
Example:
150+
151+
```ts
152+
type Example =
153+
// 'conditional' — Conditional types.
154+
(A extends B ? C : D) |
155+
// 'function' — Function types.
156+
((arg: T) => U) |
157+
// 'import' — Imported types.
158+
import('module').Type |
159+
// 'intersection' — Intersection types.
160+
(A & B) |
161+
// 'keyword' — Keyword types.
162+
any |
163+
// 'literal' — Literal types.
164+
'literal' |
165+
42 |
166+
// 'named' — Named types.
167+
SomeType |
168+
AnotherType |
169+
// 'object' — Object types.
170+
{ a: string; b: number; } |
171+
// 'operator' — Operator types.
172+
keyof T |
173+
// 'tuple' — Tuple types.
174+
[string, number] |
175+
// 'union' — Union types.
176+
(A | B) |
177+
// 'nullish' — Nullish types.
178+
null |
179+
undefined;
180+
```
181+
182+
125183
## Usage
126184

127185
<CodeTabs

rules/sort-union-types.ts

Lines changed: 113 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type { SortingNode } from '../typings'
22

33
import { createEslintRule } from '../utils/create-eslint-rule'
4+
import { getGroupNumber } from '../utils/get-group-number'
45
import { getSourceCode } from '../utils/get-source-code'
56
import { toSingleLine } from '../utils/to-single-line'
67
import { rangeToDiff } from '../utils/range-to-diff'
78
import { isPositive } from '../utils/is-positive'
9+
import { useGroups } from '../utils/use-groups'
810
import { sortNodes } from '../utils/sort-nodes'
911
import { makeFixes } from '../utils/make-fixes'
1012
import { complete } from '../utils/complete'
@@ -13,9 +15,25 @@ import { compare } from '../utils/compare'
1315

1416
type MESSAGE_ID = 'unexpectedUnionTypesOrder'
1517

18+
type Group =
19+
| 'intersection'
20+
| 'conditional'
21+
| 'function'
22+
| 'operator'
23+
| 'keyword'
24+
| 'literal'
25+
| 'nullish'
26+
| 'unknown'
27+
| 'import'
28+
| 'object'
29+
| 'named'
30+
| 'tuple'
31+
| 'union'
32+
1633
type Options = [
1734
Partial<{
1835
type: 'alphabetical' | 'line-length' | 'natural'
36+
groups: (Group[] | Group)[]
1937
order: 'desc' | 'asc'
2038
ignoreCase: boolean
2139
}>,
@@ -49,6 +67,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
4967
type: 'boolean',
5068
default: true,
5169
},
70+
groups: {
71+
type: 'array',
72+
},
5273
},
5374
additionalProperties: false,
5475
},
@@ -70,20 +91,80 @@ export default createEslintRule<Options, MESSAGE_ID>({
7091
type: 'alphabetical',
7192
ignoreCase: true,
7293
order: 'asc',
94+
groups: [],
7395
} as const)
7496

7597
let sourceCode = getSourceCode(context)
7698

77-
let nodes: SortingNode[] = node.types.map(type => ({
78-
name: sourceCode.text.slice(...type.range),
79-
size: rangeToDiff(type.range),
80-
node: type,
81-
}))
99+
let nodes: SortingNode[] = node.types.map(type => {
100+
let { getGroup, defineGroup } = useGroups(options.groups)
101+
102+
switch (type.type) {
103+
case 'TSConditionalType':
104+
defineGroup('conditional')
105+
break
106+
case 'TSFunctionType':
107+
defineGroup('function')
108+
break
109+
case 'TSImportType':
110+
defineGroup('import')
111+
break
112+
case 'TSIntersectionType':
113+
defineGroup('intersection')
114+
break
115+
case 'TSAnyKeyword':
116+
case 'TSBigIntKeyword':
117+
case 'TSBooleanKeyword':
118+
case 'TSNeverKeyword':
119+
case 'TSNumberKeyword':
120+
case 'TSObjectKeyword':
121+
case 'TSStringKeyword':
122+
case 'TSUnknownKeyword':
123+
case 'TSVoidKeyword':
124+
defineGroup('keyword')
125+
break
126+
case 'TSLiteralType':
127+
defineGroup('literal')
128+
break
129+
case 'TSTypeReference':
130+
case 'TSIndexedAccessType':
131+
defineGroup('named')
132+
break
133+
case 'TSTypeLiteral':
134+
defineGroup('object')
135+
break
136+
case 'TSTypeQuery':
137+
case 'TSTypeOperator':
138+
defineGroup('operator')
139+
break
140+
case 'TSTupleType':
141+
defineGroup('tuple')
142+
break
143+
case 'TSUnionType':
144+
defineGroup('union')
145+
break
146+
case 'TSNullKeyword':
147+
case 'TSUndefinedKeyword':
148+
defineGroup('nullish')
149+
break
150+
}
151+
152+
return {
153+
name: sourceCode.text.slice(...type.range),
154+
size: rangeToDiff(type.range),
155+
group: getGroup(),
156+
node: type,
157+
}
158+
})
82159

83160
pairwise(nodes, (left, right) => {
84-
let compareValue = isPositive(compare(left, right, options))
161+
let leftNum = getGroupNumber(options.groups, left)
162+
let rightNum = getGroupNumber(options.groups, right)
85163

86-
if (compareValue) {
164+
if (
165+
leftNum > rightNum ||
166+
(leftNum === rightNum && isPositive(compare(left, right, options)))
167+
) {
87168
context.report({
88169
messageId: 'unexpectedUnionTypesOrder',
89170
data: {
@@ -92,7 +173,31 @@ export default createEslintRule<Options, MESSAGE_ID>({
92173
},
93174
node: right.node,
94175
fix: fixer => {
95-
let sortedNodes: SortingNode[] = sortNodes(nodes, options)
176+
let grouped: {
177+
[key: string]: SortingNode[]
178+
} = {}
179+
180+
for (let currentNode of nodes) {
181+
let groupNum = getGroupNumber(options.groups, currentNode)
182+
183+
if (!(groupNum in grouped)) {
184+
grouped[groupNum] = [currentNode]
185+
} else {
186+
grouped[groupNum] = sortNodes(
187+
[...grouped[groupNum], currentNode],
188+
options,
189+
)
190+
}
191+
}
192+
193+
let sortedNodes: SortingNode[] = []
194+
195+
for (let group of Object.keys(grouped).sort(
196+
(a, b) => Number(a) - Number(b),
197+
)) {
198+
sortedNodes.push(...sortNodes(grouped[group], options))
199+
}
200+
96201
return makeFixes(fixer, nodes, sortedNodes, sourceCode)
97202
},
98203
})

0 commit comments

Comments
 (0)