Repository: azat-io/eslint-plugin-perfectionist
Pull request #469 (2025-02-13 23:18:48)

feat: add `sortBy: "value"` related-options
Should resolve https://github.com/azat-io/eslint-plugin-perfectionist/issues/68

# Description

This PR adds **3** `sort by value` related-options to `sort-object-types` and `sort-interfaces`:
- `customGroups.elementValuePattern` to match property patterns.
- `sortBy: 'name' | 'value'` (by default `name`).
- `customGroups.sortBy: 'name' | 'value'` to override the `sortBy` option in a given custom group.

# Example

Sort properties by:
- `ObjectId`.
- `Date`.
- Primitive types.
- All the rest, sorted by name alphabetically.

Users have two ways to write this: use `sortBy: 'value'` by default, and for the last group, use `sortBy: 'name'`, or the opposite.

<details>
<summary>Details</summary>

```json
{
"perfectionist/sort-object-types": [
    "error",
    {
      "groups": [
        "objectIds",
        "dates",
        "primitives"
      ],
      "customGroups": [
        {
          "groupName": "objectIds",
          "elementValuePattern": "^ObjectId$",
        },
        {
          "groupName": "dates",
          "elementValuePattern": "^Date$",
        },
        {
          "groupName": "primitives",
          "elementValuePattern": "string|number|boolean",
          "sortBy": "value"
        }
      ]
    }
  ]
}	
```

Or

```json
"perfectionist/sort-object-types": [
    "error",
    {
      "sortBy": "value",
      "groups": [
        "objectIds",
        "dates",
        "primitives",
        "allTheRestSortedNormally"
      ],
      "customGroups": [
        {
          "groupName": "objectIds",
          "elementValuePattern": "^ObjectId$"
        },
        {
          "groupName": "dates",
          "elementValuePattern": "^Date$"
        },
        {
          "groupName": "primitives",
          "elementValuePattern": "string|number|boolean"
        },
        {
          "groupName": "allTheRestSortedNormally",
          "sortBy": "name"
        }
      ]
    }
  ],
```

Result:

```ts
type Type = {
  _id: ObjectId;
  groupId: ObjectId;
  createdAt: Date;
  lastLoginAt: Date;
  b: boolean;
  c: number;
  a: string;
  // We are sorted alphabetically!
  anotherObject: AnotherObject;
  someValue: AnObject;
};
```

</details>

# Important notes

## ❓Can users sort by value alphabetically, then by name alphabetically if the values are the same ?

Currently, no. This means that 

```json
{
  "perfectionist/sort-object-types": [
    "error",
    {
      "sortBy": "value",
    }
  ],
}
```

will tolerate both

```ts
type Types = {
  a: string
  b: string
}
```

and

```ts
type Types = {
  b: string
  a: string
}
```

We would need to allow users to write

```ts
"perfectionist/sort-object-types": [
    "error",
    {
      "sortBy": "value",
	  "fallbackSort": { "sortBy": "name"}
    }
  ],
```

This can come in a later PR.

## ❓How does `sortBy: 'value'` work with **non-properties** ?

It depends if it's the base `sortBy` option, or the `customGroups` one.

### Base `sortBy` option

Non-properties nodes will **not** have a sort order enforced.

```ts
type Types = {
  z(): void
  a: string
}
```
and 
```ts
type Types = {
  b: string
  z(): void
}
```

are both allowed.

### `customGroups.sortBy` option

**Grouping** is still enabled, but within the group, the sort order is not enforced. This essentially means that we are enforcing the non-property member (such as functions) matching the group to be in that group, but the user can choose to move that member inside that group wherever they want.

## Commit split

- Commits **1-5** are refactors.
- Commit **6+** are the actual implementation.

# What is the purpose of this pull request?

- [x] New Feature


azat-io: LGTM.

In the beginning, I thought about using Typescript to solve this problem.

To have TypeScript ESLint detect the type of the value and sort them according to the type. For example, if it is a JSX props object. And the types are specified somewhere else.

```ts
interface Props {
  isOpen: boolean
  isCompact: boolean
  otherProp: string
  value: string
  onChange: () => {}
}

// For example if booleans should be last

export let Component: FC<Props> = ({
  onChange,
  otherProp
  value,
  isCompact,
  isOpen,
}) => {}
```

P.S. At least I want to try to do it in another plugin:
https://github.com/azat-io/eslint-plugin-de-morgan/pull/3



hugop95: @azat-io I think that we would need to dive deeper into how `typescript-eslint` infers typings.

I haven't had the time to l look into that, so I don't know if it's going to end up being hard to implement or not. The benefits can be quite big for a lot of rules from `perfectionist`, and for your new `de-morgan` plugin as well. It's something I can start looking into as well once the whole `sortBy: value` milestone is done. 🙂
azat-io: The benefits can be quite big, but I'm afraid it could affect the performance.
hugop95: @azat-io 

> The benefits can be quite big, but I'm afraid it could affect the performance.

It's always something we can put behind an option, similarly to `sort-imports` and `tsconfig` parsing!
azat-io: Yes 👍 

Patch:
diff --git a/docs/content/rules/sort-interfaces.mdx b/docs/content/rules/sort-interfaces.mdx
index 2a758f6..0827740 100644
--- a/docs/content/rules/sort-interfaces.mdx
+++ b/docs/content/rules/sort-interfaces.mdx
@@ -202,6 +202,36 @@ Controls whether special characters should be trimmed, removed or kept before so
 - `'trim'` — Trim special characters when sorting alphabetically or naturally (e.g., “_a” and “a” are the same).
 - `'remove'` — Remove special characters when sorting (e.g., “/a/b” and “ab” are the same).
 
+### sortBy
+
+<sub>default: `name`</sub>
+
+Controls whether sorting should be done only using the interface's values.
+
+- `name` — Use interface types keys.
+- `value` — Use the values of properties. Non-properties will not have an enforced sort order.
+
+Example
+
+```ts
+interface User {
+  createdAt: Date
+  lastLoginAt: Date
+  _id: ObjectId
+  groupId: ObjectId
+  city: string
+}
+```
+
+`sortBy` option configuration:
+
+```js
+{
+  sortBy: 'value',
+}
+
+```
+
 ### locales
 
 <sub>default: `'en-US'`</sub>
@@ -524,10 +554,12 @@ interface CustomGroupDefinition {
   groupName: string
   type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted'
   order?: 'asc' | 'desc'
+  sortBy?: 'name' | 'value'
   newlinesInside?: 'always' | 'never'
   selector?: string
   modifiers?: string[]
   elementNamePattern?: string | string[] | { pattern: string; flags?: string } | { pattern: string; flags?: string }[]
+  elementValuePattern?: string | string[] | { pattern: string; flags?: string } | { pattern: string; flags?: string }[]
 }
 
 ```
@@ -540,11 +572,13 @@ interface CustomGroupAnyOfDefinition {
   groupName: string
   type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted'
   order?: 'asc' | 'desc'
+  sortBy?: 'name' | 'value'
   newlinesInside?: 'always' | 'never'
   anyOf: Array<{
       selector?: string
       modifiers?: string[]
       elementNamePattern?: string | string[] | { pattern: string; flags?: string } | { pattern: string; flags?: string }[]
+      elementValuePattern?: string | string[] | { pattern: string; flags?: string } | { pattern: string; flags?: string }[]
   }>
 }
 ```
@@ -557,8 +591,10 @@ An interface member will match a `CustomGroupAnyOfDefinition` group if it matche
 - `selector`: Filter on the `selector` of the element.
 - `modifiers`: Filter on the `modifiers` of the element. (All the modifiers of the element must be present in that list)
 - `elementNamePattern`: If entered, will check that the name of the element matches the pattern entered.
+- `elementValuePattern`: Only for properties. If entered, will check that the value of the property matches the pattern entered.
 - `type`: Overrides the sort type for that custom group. `unsorted` will not sort the group.
 - `order`: Overrides the sort order for that custom group
+- `sortBy`: Overrides the `sortBy` option for that custom group
 - `newlinesInside`: Enforces a specific newline behavior between elements of the group.
 
 #### Match importance
@@ -634,6 +670,7 @@ interface User {
                   fallbackSort: { type: 'unsorted' },
                   ignoreCase: true,
                   specialCharacters: 'keep',
+                  sortBy: 'name',
                   ignorePattern: [],
                   partitionByComment: false,
                   partitionByNewLine: false,
@@ -667,6 +704,7 @@ interface User {
                 fallbackSort: { type: 'unsorted' },
                 ignoreCase: true,
                 specialCharacters: 'keep',
+                sortBy: 'name',
                 ignorePattern: [],
                 partitionByComment: false,
                 partitionByNewLine: false,
diff --git a/docs/content/rules/sort-object-types.mdx b/docs/content/rules/sort-object-types.mdx
index 3baf46a..b6dde35 100644
--- a/docs/content/rules/sort-object-types.mdx
+++ b/docs/content/rules/sort-object-types.mdx
@@ -164,6 +164,36 @@ Controls whether special characters should be trimmed, removed or kept before so
 - `'trim'` — Trim special characters when sorting alphabetically or naturally (e.g., “_a” and “a” are the same).
 - `'remove'` — Remove special characters when sorting (e.g., “/a/b” and “ab” are the same).
 
+### sortBy
+
+<sub>default: `name`</sub>
+
+Controls whether sorting should be done only using the object type's values.
+
+- `name` — Use object types keys.
+- `value` — Use the values of properties. Non-properties will not have an enforced sort order.
+
+Example
+
+```ts
+type User = {
+  createdAt: Date
+  lastLoginAt: Date
+  _id: ObjectId
+  groupId: ObjectId
+  city: string
+}
+```
+
+`sortBy` option configuration:
+
+```js
+{
+  sortBy: 'value',
+}
+
+```
+
 ### locales
 
 <sub>default: `'en-US'`</sub>
@@ -471,10 +501,12 @@ interface CustomGroupDefinition {
   groupName: string
   type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted'
   order?: 'asc' | 'desc'
+  sortBy?: 'name' | 'value'
   newlinesInside?: 'always' | 'never'
   selector?: string
   modifiers?: string[]
   elementNamePattern?: string | string[] | { pattern: string; flags?: string } | { pattern: string; flags?: string }[]
+  elementValuePattern?: string | string[] | { pattern: string; flags?: string } | { pattern: string; flags?: string }[]
 }
 
 ```
@@ -487,11 +519,13 @@ interface CustomGroupAnyOfDefinition {
   groupName: string
   type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted'
   order?: 'asc' | 'desc'
+  sortBy?: 'name' | 'value'
   newlinesInside?: 'always' | 'never'
   anyOf: Array<{
       selector?: string
       modifiers?: string[]
       elementNamePattern?: string | string[] | { pattern: string; flags?: string } | { pattern: string; flags?: string }[]
+      elementValuePattern?: string | string[] | { pattern: string; flags?: string } | { pattern: string; flags?: string }[]
   }>
 }
 ```
@@ -504,8 +538,10 @@ An object type will match a `CustomGroupAnyOfDefinition` group if it matches all
 - `selector`: Filter on the `selector` of the element.
 - `modifiers`: Filter on the `modifiers` of the element. (All the modifiers of the element must be present in that list)
 - `elementNamePattern`: If entered, will check that the name of the element matches the pattern entered.
+- `elementValuePattern`: Only for properties. If entered, will check that the value of the property matches the pattern entered.
 - `type`: Overrides the sort type for that custom group. `unsorted` will not sort the group.
 - `order`: Overrides the sort order for that custom group
+- `sortBy`: Overrides the `sortBy` option for that custom group
 - `newlinesInside`: Enforces a specific newline behavior between elements of the group.
 
 #### Match importance
@@ -600,6 +636,7 @@ This feature is only applicable when `partitionByNewLine` is false.
                   fallbackSort: { type: 'unsorted' },
                   ignoreCase: true,
                   specialCharacters: 'keep',
+                  sortBy: 'name',
                   ignorePattern: [],
                   partitionByComment: false,
                   partitionByNewLine: false,
@@ -632,6 +669,7 @@ This feature is only applicable when `partitionByNewLine` is false.
                 fallbackSort: { type: 'unsorted' },
                 ignoreCase: true,
                 specialCharacters: 'keep',
+                sortBy: 'name',
                 ignorePattern: [],
                 partitionByComment: false,
                 partitionByNewLine: false,
diff --git a/rules/sort-array-includes/types.ts b/rules/sort-array-includes/types.ts
index 349579d..2b6ec63 100644
--- a/rules/sort-array-includes/types.ts
+++ b/rules/sort-array-includes/types.ts
@@ -7,7 +7,6 @@ import type {
   CommonOptions,
   GroupsOptions,
   RegexOption,
-  TypeOption,
 } from '../../types/common-options'
 
 import {
@@ -29,7 +28,6 @@ export type Options = Partial<
     newlinesBetween: NewlinesBetweenOption
     groups: GroupsOptions<Group>
     partitionByNewLine: boolean
-    type: TypeOption
   } & CommonOptions
 >[]
 
diff --git a/rules/sort-classes/types.ts b/rules/sort-classes/types.ts
index c8265f7..2d2c27f 100644
--- a/rules/sort-classes/types.ts
+++ b/rules/sort-classes/types.ts
@@ -7,7 +7,6 @@ import type {
   CommonOptions,
   GroupsOptions,
   RegexOption,
-  TypeOption,
 } from '../../types/common-options'
 import type { JoinWithDash } from '../../types/join-with-dash'
 
@@ -37,7 +36,6 @@ export type SortClassesOptions = [
       newlinesBetween: NewlinesBetweenOption
       groups: GroupsOptions<Group>
       partitionByNewLine: boolean
-      type: TypeOption
     } & CommonOptions
   >,
 ]
diff --git a/rules/sort-decorators.ts b/rules/sort-decorators.ts
index f63d722..909cbe4 100644
--- a/rules/sort-decorators.ts
+++ b/rules/sort-decorators.ts
@@ -6,7 +6,6 @@ import type {
   PartitionByCommentOption,
   CommonOptions,
   GroupsOptions,
-  TypeOption,
 } from '../types/common-options'
 import type { SortingNode } from '../types/sorting-node'
 
@@ -45,7 +44,6 @@ export type Options = [
       sortOnAccessors: boolean
       sortOnMethods: boolean
       sortOnClasses: boolean
-      type: TypeOption
     } & CommonOptions
   >,
 ]
diff --git a/rules/sort-enums.ts b/rules/sort-enums.ts
index 8a20e76..5964a68 100644
--- a/rules/sort-enums.ts
+++ b/rules/sort-enums.ts
@@ -51,6 +51,7 @@ type MESSAGE_ID =
 interface SortEnumsSortingNode
   extends SortingNodeWithDependencies<TSESTree.TSEnumMember> {
   numericValue: number | null
+  value: string | null
 }
 
 let defaultOptions: Required<Options[0]> = {
@@ -173,6 +174,10 @@ export default createEslintRule<Options, MESSAGE_ID>({
                   member.initializer,
                 ) /* v8 ignore next - Unsure how we can reach that case */
               : null,
+            value:
+              member.initializer?.type === 'Literal'
+                ? (member.initializer.value?.toString() ?? null)
+                : null,
             isEslintDisabled: isNodeEslintDisabled(member, eslintDisabledLines),
             size: rangeToDiff(member, sourceCode),
             group: getGroup(),
@@ -216,10 +221,7 @@ export default createEslintRule<Options, MESSAGE_ID>({
                 if (isNumericEnum) {
                   return sortingNode.numericValue!.toString()
                 }
-                if (sortingNode.node.initializer?.type === 'Literal') {
-                  return sortingNode.node.initializer.value?.toString() ?? ''
-                }
-                return ''
+                return sortingNode.value ?? ''
               }
             : null,
         /**
diff --git a/rules/sort-enums/types.ts b/rules/sort-enums/types.ts
index ada7e4a..2cc0b40 100644
--- a/rules/sort-enums/types.ts
+++ b/rules/sort-enums/types.ts
@@ -7,7 +7,6 @@ import type {
   CommonOptions,
   GroupsOptions,
   RegexOption,
-  TypeOption,
 } from '../../types/common-options'
 
 import { regexJsonSchema } from '../../utils/common-json-schemas'
@@ -21,7 +20,6 @@ export type Options = Partial<
     partitionByNewLine: boolean
     forceNumericSort: boolean
     sortByValue: boolean
-    type: TypeOption
   } & CommonOptions
 >[]
 
diff --git a/rules/sort-exports.ts b/rules/sort-exports.ts
index 177e8a8..fb5cbd9 100644
--- a/rules/sort-exports.ts
+++ b/rules/sort-exports.ts
@@ -3,7 +3,6 @@ import type { TSESTree } from '@typescript-eslint/types'
 import type {
   PartitionByCommentOption,
   CommonOptions,
-  TypeOption,
 } from '../types/common-options'
 import type { SortingNode } from '../types/sorting-node'
 
@@ -31,7 +30,6 @@ type Options = [
       groupKind: 'values-first' | 'types-first' | 'mixed'
       partitionByComment: PartitionByCommentOption
       partitionByNewLine: boolean
-      type: TypeOption
     } & CommonOptions
   >,
 ]
diff --git a/rules/sort-heritage-clauses.ts b/rules/sort-heritage-clauses.ts
index 6dfa69c..73b95e4 100644
--- a/rules/sort-heritage-clauses.ts
+++ b/rules/sort-heritage-clauses.ts
@@ -5,7 +5,6 @@ import type {
   DeprecatedCustomGroupsOption,
   CommonOptions,
   GroupsOptions,
-  TypeOption,
 } from '../types/common-options'
 import type { SortingNode } from '../types/sorting-node'
 
@@ -34,7 +33,6 @@ export type Options = [
     {
       customGroups: DeprecatedCustomGroupsOption
       groups: GroupsOptions<Group>
-      type: TypeOption
     } & CommonOptions
   >,
 ]
diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts
index 7a5200e..ba51d36 100644
--- a/rules/sort-imports.ts
+++ b/rules/sort-imports.ts
@@ -510,7 +510,12 @@ export default createEslintRule<Options, MESSAGE_ID>({
                   return options
                 }
                 let group = options.groups[groupNumber]
-                return isSideEffectOnlyGroup(group) ? null : options
+                return {
+                  ...options,
+                  type: isSideEffectOnlyGroup(group)
+                    ? 'unsorted'
+                    : options.type,
+                }
               },
               isNodeIgnored: node => node.isIgnored,
               ignoreEslintDisabledNodes,
diff --git a/rules/sort-interfaces.ts b/rules/sort-interfaces.ts
index b27dbec..f6c3a03 100644
--- a/rules/sort-interfaces.ts
+++ b/rules/sort-interfaces.ts
@@ -30,6 +30,7 @@ let defaultOptions: Required<Options[0]> = {
   ignoreCase: true,
   customGroups: {},
   locales: 'en-US',
+  sortBy: 'name',
   alphabet: '',
   order: 'asc',
   groups: [],
diff --git a/rules/sort-intersection-types.ts b/rules/sort-intersection-types.ts
index b443d07..a05872d 100644
--- a/rules/sort-intersection-types.ts
+++ b/rules/sort-intersection-types.ts
@@ -6,9 +6,8 @@ import {
   GROUP_ORDER_ERROR,
   ORDER_ERROR,
 } from '../utils/report-errors'
-import { sortUnionOrIntersectionTypes } from './sort-union-types'
+import { sortUnionOrIntersectionTypes, jsonSchema } from './sort-union-types'
 import { createEslintRule } from '../utils/create-eslint-rule'
-import { jsonSchema } from './sort-union-types'
 
 type MESSAGE_ID =
   | 'missedSpacingBetweenIntersectionTypes'
diff --git a/rules/sort-jsx-props.ts b/rules/sort-jsx-props.ts
index 20bb1c5..86f0f32 100644
--- a/rules/sort-jsx-props.ts
+++ b/rules/sort-jsx-props.ts
@@ -6,7 +6,6 @@ import type {
   CommonOptions,
   GroupsOptions,
   RegexOption,
-  TypeOption,
 } from '../types/common-options'
 import type { SortingNode } from '../types/sorting-node'
 
@@ -57,7 +56,6 @@ type Options = Partial<
      * @deprecated for {@link `useConfigurationIf.tagMatchesPattern`}
      */
     ignorePattern: RegexOption
-    type: TypeOption
   } & CommonOptions
 >[]
 
diff --git a/rules/sort-maps/types.ts b/rules/sort-maps/types.ts
index e2f0a2a..dac5e36 100644
--- a/rules/sort-maps/types.ts
+++ b/rules/sort-maps/types.ts
@@ -7,7 +7,6 @@ import type {
   CommonOptions,
   GroupsOptions,
   RegexOption,
-  TypeOption,
 } from '../../types/common-options'
 
 import { regexJsonSchema } from '../../utils/common-json-schemas'
@@ -22,7 +21,6 @@ export type Options = Partial<
     newlinesBetween: NewlinesBetweenOption
     groups: GroupsOptions<Group>
     partitionByNewLine: boolean
-    type: TypeOption
   } & CommonOptions
 >[]
 
diff --git a/rules/sort-modules/types.ts b/rules/sort-modules/types.ts
index 04eff4a..44a44d0 100644
--- a/rules/sort-modules/types.ts
+++ b/rules/sort-modules/types.ts
@@ -7,7 +7,6 @@ import type {
   CommonOptions,
   GroupsOptions,
   RegexOption,
-  TypeOption,
 } from '../../types/common-options'
 import type { JoinWithDash } from '../../types/join-with-dash'
 
@@ -35,7 +34,6 @@ export type SortModulesOptions = [
       newlinesBetween: NewlinesBetweenOption
       groups: GroupsOptions<Group>
       partitionByNewLine: boolean
-      type: TypeOption
     } & CommonOptions
   >,
 ]
diff --git a/rules/sort-named-exports.ts b/rules/sort-named-exports.ts
index e4d4d1f..b9a8726 100644
--- a/rules/sort-named-exports.ts
+++ b/rules/sort-named-exports.ts
@@ -3,7 +3,6 @@ import type { TSESTree } from '@typescript-eslint/types'
 import type {
   PartitionByCommentOption,
   CommonOptions,
-  TypeOption,
 } from '../types/common-options'
 import type { SortingNode } from '../types/sorting-node'
 
@@ -33,7 +32,6 @@ type Options = [
       partitionByComment: PartitionByCommentOption
       partitionByNewLine: boolean
       ignoreAlias: boolean
-      type: TypeOption
     } & CommonOptions
   >,
 ]
diff --git a/rules/sort-named-imports.ts b/rules/sort-named-imports.ts
index 1886a37..29694ca 100644
--- a/rules/sort-named-imports.ts
+++ b/rules/sort-named-imports.ts
@@ -3,7 +3,6 @@ import type { TSESTree } from '@typescript-eslint/types'
 import type {
   PartitionByCommentOption,
   CommonOptions,
-  TypeOption,
 } from '../types/common-options'
 import type { SortingNode } from '../types/sorting-node'
 
@@ -33,7 +32,6 @@ type Options = [
       partitionByComment: PartitionByCommentOption
       partitionByNewLine: boolean
       ignoreAlias: boolean
-      type: TypeOption
     } & CommonOptions
   >,
 ]
diff --git a/rules/sort-object-types.ts b/rules/sort-object-types.ts
index 3446dc7..e8a263e 100644
--- a/rules/sort-object-types.ts
+++ b/rules/sort-object-types.ts
@@ -3,8 +3,13 @@ import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'
 import type { TSESTree } from '@typescript-eslint/types'
 import type { TSESLint } from '@typescript-eslint/utils'
 
-import type { Modifier, Selector, Options } from './sort-object-types/types'
-import type { SortingNode } from '../types/sorting-node'
+import type {
+  SortObjectTypesSortingNode,
+  Modifier,
+  Selector,
+  Options,
+} from './sort-object-types/types'
+import type { CompareOptions } from '../utils/compare'
 
 import {
   buildUseConfigurationIfJsonSchema,
@@ -30,9 +35,10 @@ import {
   allSelectors,
 } from './sort-object-types/types'
 import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration'
+import { getCustomGroupsCompareOptions } from './sort-object-types/get-custom-groups-compare-options'
 import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration'
-import { getCustomGroupsCompareOptions } from '../utils/get-custom-groups-compare-options'
 import { doesCustomGroupMatch } from './sort-object-types/does-custom-group-match'
+import { buildNodeValueGetter } from './sort-object-types/build-node-value-getter'
 import { getMatchingContextOptions } from '../utils/get-matching-context-options'
 import { generatePredefinedGroups } from '../utils/generate-predefined-groups'
 import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
@@ -41,6 +47,7 @@ import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
 import { isNodeFunctionType } from '../utils/is-node-function-type'
 import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
 import { createEslintRule } from '../utils/create-eslint-rule'
+import { sortByJsonSchema } from './sort-object-types/types'
 import { reportAllErrors } from '../utils/report-all-errors'
 import { shouldPartition } from '../utils/should-partition'
 import { getSourceCode } from '../utils/get-source-code'
@@ -62,10 +69,6 @@ type MESSAGE_ID =
   | 'unexpectedObjectTypesGroupOrder'
   | 'unexpectedObjectTypesOrder'
 
-interface SortObjectTypesSortingNode extends SortingNode<TSESTree.TypeElement> {
-  groupKind: 'required' | 'optional'
-}
-
 let defaultOptions: Required<Options[0]> = {
   fallbackSort: { type: 'unsorted' },
   partitionByComment: false,
@@ -79,6 +82,7 @@ let defaultOptions: Required<Options[0]> = {
   ignoreCase: true,
   customGroups: {},
   locales: 'en-US',
+  sortBy: 'name',
   alphabet: '',
   order: 'asc',
   groups: [],
@@ -108,6 +112,7 @@ export let jsonSchema: JSONSchema4 = {
       partitionByNewLine: partitionByNewLineJsonSchema,
       newlinesBetween: newlinesBetweenJsonSchema,
       ignorePattern: regexJsonSchema,
+      sortBy: sortByJsonSchema,
       groups: groupsJsonSchema,
     },
     additionalProperties: false,
@@ -270,10 +275,19 @@ export let sortObjectTypeElements = <MessageIds extends string>({
       }
 
       let name = getNodeName({ typeElement, sourceCode })
+      let value: string | null = null
+      if (
+        typeElement.type === 'TSPropertySignature' &&
+        typeElement.typeAnnotation
+      ) {
+        value = sourceCode.getText(typeElement.typeAnnotation.typeAnnotation)
+      }
+
       if (Array.isArray(options.customGroups)) {
         for (let customGroup of options.customGroups) {
           if (
             doesCustomGroupMatch({
+              elementValue: value,
               elementName: name,
               customGroup,
               selectors,
@@ -306,6 +320,7 @@ export let sortObjectTypeElements = <MessageIds extends string>({
         addSafetySemicolonWhenInline: true,
         group: getGroup(),
         node: typeElement,
+        value,
         name,
       }
 
@@ -343,13 +358,25 @@ export let sortObjectTypeElements = <MessageIds extends string>({
           groupKind === 'any' || currentNode.groupKind === groupKind,
       ),
     )
+
+    let compareOptions: CompareOptions<SortObjectTypesSortingNode> &
+      Required<Options[0]> = {
+      ...options,
+      nodeValueGetter: buildNodeValueGetter(options.sortBy),
+    }
     let sortNodesExcludingEslintDisabled = (
       ignoreEslintDisabledNodes: boolean,
     ): SortObjectTypesSortingNode[] =>
       filteredGroupKindNodes.flatMap(groupedNodes =>
-        sortNodesByGroups(groupedNodes, options, {
+        sortNodesByGroups(groupedNodes, compareOptions, {
+          isNodeIgnoredForGroup: (node, groupCompareOptions) => {
+            if (groupCompareOptions.sortBy === 'value') {
+              return !node.value
+            }
+            return false
+          },
           getGroupCompareOptions: groupNumber =>
-            getCustomGroupsCompareOptions(options, groupNumber),
+            getCustomGroupsCompareOptions(compareOptions, groupNumber),
           ignoreEslintDisabledNodes,
         }),
       )
diff --git a/rules/sort-object-types/build-node-value-getter.ts b/rules/sort-object-types/build-node-value-getter.ts
new file mode 100644
index 0000000..e2feb89
--- /dev/null
+++ b/rules/sort-object-types/build-node-value-getter.ts
@@ -0,0 +1,6 @@
+import type { SortObjectTypesSortingNode } from './types'
+
+export let buildNodeValueGetter = (
+  sortBy: 'value' | 'name',
+): ((node: SortObjectTypesSortingNode) => string) | null =>
+  sortBy === 'value' ? node => node.value ?? '' : null
diff --git a/rules/sort-object-types/does-custom-group-match.ts b/rules/sort-object-types/does-custom-group-match.ts
index 304f456..5d46bd4 100644
--- a/rules/sort-object-types/does-custom-group-match.ts
+++ b/rules/sort-object-types/does-custom-group-match.ts
@@ -5,6 +5,7 @@ import { matches } from '../../utils/matches'
 
 interface DoesCustomGroupMatchParameters {
   customGroup: AnyOfCustomGroup<SingleCustomGroup> | SingleCustomGroup
+  elementValue: string | null
   selectors: Selector[]
   modifiers: Modifier[]
   elementName: string
@@ -54,5 +55,18 @@ export let doesCustomGroupMatch = (
     }
   }
 
+  if (
+    'elementValuePattern' in props.customGroup &&
+    props.customGroup.elementValuePattern
+  ) {
+    let matchesElementValuePattern: boolean = matches(
+      props.elementValue ?? '',
+      props.customGroup.elementValuePattern,
+    )
+    if (!matchesElementValuePattern) {
+      return false
+    }
+  }
+
   return true
 }
diff --git a/rules/sort-object-types/get-custom-groups-compare-options.ts b/rules/sort-object-types/get-custom-groups-compare-options.ts
new file mode 100644
index 0000000..03fdb02
--- /dev/null
+++ b/rules/sort-object-types/get-custom-groups-compare-options.ts
@@ -0,0 +1,32 @@
+import type { SortObjectTypesSortingNode, Options } from './types'
+import type { CompareOptions } from '../../utils/compare'
+
+import { buildNodeValueGetter } from './build-node-value-getter'
+
+export let getCustomGroupsCompareOptions = (
+  options: Required<Options[0]>,
+  groupNumber: number,
+): CompareOptions<SortObjectTypesSortingNode> & Required<Options[0]> => {
+  if (!Array.isArray(options.customGroups)) {
+    return options
+  }
+  let group = options.groups[groupNumber]
+  let customGroup =
+    typeof group === 'string'
+      ? options.customGroups.find(
+          currentGroup => group === currentGroup.groupName,
+        )
+      : null
+
+  let sortBy =
+    customGroup && 'sortBy' in customGroup ? customGroup.sortBy : options.sortBy
+  sortBy ??= options.sortBy
+
+  return {
+    ...options,
+    nodeValueGetter: buildNodeValueGetter(sortBy),
+    order: customGroup?.order ?? options.order,
+    type: customGroup?.type ?? options.type,
+    sortBy,
+  }
+}
diff --git a/rules/sort-object-types/types.ts b/rules/sort-object-types/types.ts
index 05e82ab..8e49408 100644
--- a/rules/sort-object-types/types.ts
+++ b/rules/sort-object-types/types.ts
@@ -1,4 +1,5 @@
 import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'
+import type { TSESTree } from '@typescript-eslint/types'
 
 import type {
   DeprecatedCustomGroupsOption,
@@ -8,9 +9,9 @@ import type {
   CommonOptions,
   GroupsOptions,
   RegexOption,
-  TypeOption,
 } from '../../types/common-options'
 import type { JoinWithDash } from '../../types/join-with-dash'
+import type { SortingNode } from '../../types/sorting-node'
 
 import {
   buildCustomGroupModifiersJsonSchema,
@@ -39,14 +40,17 @@ export type Options = Partial<
      * @deprecated for {@link `useConfigurationIf.declarationMatchesPattern`}
      */
     ignorePattern: RegexOption
-    type: TypeOption
+    sortBy: 'value' | 'name'
   } & CommonOptions
 >[]
 
 export type SingleCustomGroup = (
+  | ({
+      elementValuePattern?: RegexOption
+      sortBy?: 'value' | 'name'
+    } & BaseSingleCustomGroup<PropertySelector>)
   | BaseSingleCustomGroup<IndexSignatureSelector>
   | BaseSingleCustomGroup<MultilineSelector>
-  | BaseSingleCustomGroup<PropertySelector>
   | BaseSingleCustomGroup<MethodSelector>
   | BaseSingleCustomGroup<MemberSelector>
 ) &
@@ -150,6 +154,11 @@ export let allSelectors: Selector[] = [
 
 export let allModifiers: Modifier[] = ['optional', 'required', 'multiline']
 
+export let sortByJsonSchema: JSONSchema4 = {
+  enum: ['name', 'value'],
+  type: 'string',
+}
+
 /**
  * Ideally, we should generate as many schemas as there are selectors, and ensure
  * that users do not enter invalid modifiers for a given selector
@@ -157,5 +166,13 @@ export let allModifiers: Modifier[] = ['optional', 'required', 'multiline']
 export let singleCustomGroupJsonSchema: Record<string, JSONSchema4> = {
   modifiers: buildCustomGroupModifiersJsonSchema(allModifiers),
   selector: buildCustomGroupSelectorJsonSchema(allSelectors),
+  elementValuePattern: regexJsonSchema,
   elementNamePattern: regexJsonSchema,
+  sortBy: sortByJsonSchema,
+}
+
+export interface SortObjectTypesSortingNode
+  extends SortingNode<TSESTree.TypeElement> {
+  groupKind: 'required' | 'optional'
+  value: string | null
 }
diff --git a/rules/sort-objects.ts b/rules/sort-objects.ts
index 695f427..e72985a 100644
--- a/rules/sort-objects.ts
+++ b/rules/sort-objects.ts
@@ -1,8 +1,7 @@
 import { TSESTree } from '@typescript-eslint/types'
 
 import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-dependencies'
-import type { Modifier, Selector } from './sort-objects/types'
-import type { Options } from './sort-objects/types'
+import type { Modifier, Selector, Options } from './sort-objects/types'
 
 import {
   buildUseConfigurationIfJsonSchema,
@@ -24,6 +23,11 @@ import {
 } from '../utils/report-errors'
 import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration'
 import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration'
+import {
+  singleCustomGroupJsonSchema,
+  allModifiers,
+  allSelectors,
+} from './sort-objects/types'
 import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration'
 import { getFirstNodeParentWithType } from './sort-objects/get-first-node-parent-with-type'
 import { getCustomGroupsCompareOptions } from '../utils/get-custom-groups-compare-options'
@@ -33,9 +37,7 @@ import { sortNodesByDependencies } from '../utils/sort-nodes-by-dependencies'
 import { doesCustomGroupMatch } from './sort-objects/does-custom-group-match'
 import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
 import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
-import { singleCustomGroupJsonSchema } from './sort-objects/types'
 import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
-import { allModifiers, allSelectors } from './sort-objects/types'
 import { createEslintRule } from '../utils/create-eslint-rule'
 import { reportAllErrors } from '../utils/report-all-errors'
 import { shouldPartition } from '../utils/should-partition'
diff --git a/rules/sort-objects/types.ts b/rules/sort-objects/types.ts
index 2f37d66..fb4f493 100644
--- a/rules/sort-objects/types.ts
+++ b/rules/sort-objects/types.ts
@@ -8,7 +8,6 @@ import type {
   CommonOptions,
   GroupsOptions,
   RegexOption,
-  TypeOption,
 } from '../../types/common-options'
 import type { JoinWithDash } from '../../types/join-with-dash'
 
@@ -39,7 +38,6 @@ export type Options = Partial<
      * @deprecated for {@link `destructuredObjects`} and {@link `objectDeclarations`}
      */
     destructureOnly: boolean
-    type: TypeOption
   } & CommonOptions
 >[]
 
diff --git a/rules/sort-switch-case.ts b/rules/sort-switch-case.ts
index 41d6409..71f5c35 100644
--- a/rules/sort-switch-case.ts
+++ b/rules/sort-switch-case.ts
@@ -1,7 +1,7 @@
 import type { TSESTree } from '@typescript-eslint/types'
 import type { TSESLint } from '@typescript-eslint/utils'
 
-import type { CommonOptions, TypeOption } from '../types/common-options'
+import type { CommonOptions } from '../types/common-options'
 import type { SortingNode } from '../types/sorting-node'
 
 import { makeSingleNodeCommentAfterFixes } from '../utils/make-single-node-comment-after-fixes'
@@ -24,16 +24,10 @@ interface SortSwitchCaseSortingNode extends SortingNode<TSESTree.SwitchCase> {
   isDefaultClause: boolean
 }
 
-type Options = [
-  Partial<
-    {
-      type: TypeOption
-    } & CommonOptions
-  >,
-]
-
 type MESSAGE_ID = 'unexpectedSwitchCaseOrder'
 
+type Options = [Partial<CommonOptions>]
+
 let defaultOptions: Required<Options[0]> = {
   fallbackSort: { type: 'unsorted' },
   specialCharacters: 'keep',
diff --git a/rules/sort-union-types.ts b/rules/sort-union-types.ts
index c77d199..3422f46 100644
--- a/rules/sort-union-types.ts
+++ b/rules/sort-union-types.ts
@@ -7,7 +7,6 @@ import type {
   NewlinesBetweenOption,
   CommonOptions,
   GroupsOptions,
-  TypeOption,
 } from '../types/common-options'
 import type { SortingNode } from '../types/sorting-node'
 
@@ -46,7 +45,6 @@ export type Options = [
       newlinesBetween: NewlinesBetweenOption
       groups: GroupsOptions<Group>
       partitionByNewLine: boolean
-      type: TypeOption
     } & CommonOptions
   >,
 ]
diff --git a/rules/sort-variable-declarations.ts b/rules/sort-variable-declarations.ts
index c2af70b..0a7851a 100644
--- a/rules/sort-variable-declarations.ts
+++ b/rules/sort-variable-declarations.ts
@@ -3,7 +3,6 @@ import type { TSESTree } from '@typescript-eslint/types'
 import type {
   PartitionByCommentOption,
   CommonOptions,
-  TypeOption,
 } from '../types/common-options'
 import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-dependencies'
 
@@ -32,7 +31,6 @@ type Options = [
     {
       partitionByComment: PartitionByCommentOption
       partitionByNewLine: boolean
-      type: TypeOption
     } & CommonOptions
   >,
 ]
diff --git a/types/common-options.ts b/types/common-options.ts
index adf9822..5997db1 100644
--- a/types/common-options.ts
+++ b/types/common-options.ts
@@ -1,6 +1,5 @@
 export type CustomGroupsOption<SingleCustomGroup = object> = ({
   newlinesInside?: 'always' | 'never'
-  fallbackSort?: FallbackSortOption
   groupName: string
 } & (AnyOfCustomGroup<SingleCustomGroup> | SingleCustomGroup) & {
     order?: OrderOption
@@ -14,6 +13,7 @@ export interface CommonOptions {
   ignoreCase: boolean
   order: OrderOption
   alphabet: string
+  type: TypeOption
 }
 
 export type PartitionByCommentOption =
diff --git a/utils/common-json-schemas.ts b/utils/common-json-schemas.ts
index e793fcb..89a08d9 100644
--- a/utils/common-json-schemas.ts
+++ b/utils/common-json-schemas.ts
@@ -190,33 +190,19 @@ export let buildUseConfigurationIfJsonSchema = ({
   type: 'object',
 })
 
-let customGroupSortJsonSchema: Record<string, JSONSchema4> = {
-  type: {
-    enum: ['alphabetical', 'line-length', 'natural', 'unsorted'],
-    description: 'Custom group sort type.',
-    type: 'string',
-  },
-  order: {
-    description: 'Custom group sort order.',
-    enum: ['desc', 'asc'],
-    type: 'string',
-  },
-}
-
-let customGroupNameJsonSchema: Record<string, JSONSchema4> = {
-  groupName: {
-    description: 'Custom group name.',
-    type: 'string',
-  },
-}
-
-let customGroupNewlinesInsideJsonSchema: Record<string, JSONSchema4> = {
+let commonCustomGroupJsonSchemas: Record<string, JSONSchema4> = {
   newlinesInside: {
     description:
       'Specifies how new lines should be handled between members of the custom group.',
     enum: ['always', 'never'],
     type: 'string',
   },
+  groupName: {
+    description: 'Custom group name.',
+    type: 'string',
+  },
+  order: orderJsonSchema,
+  type: typeJsonSchema,
 }
 
 export let buildCustomGroupsArrayJsonSchema = ({
@@ -228,9 +214,7 @@ export let buildCustomGroupsArrayJsonSchema = ({
     oneOf: [
       {
         properties: {
-          ...customGroupNameJsonSchema,
-          ...customGroupSortJsonSchema,
-          ...customGroupNewlinesInsideJsonSchema,
+          ...commonCustomGroupJsonSchemas,
           anyOf: {
             items: {
               properties: {
@@ -249,9 +233,7 @@ export let buildCustomGroupsArrayJsonSchema = ({
       },
       {
         properties: {
-          ...customGroupNameJsonSchema,
-          ...customGroupSortJsonSchema,
-          ...customGroupNewlinesInsideJsonSchema,
+          ...commonCustomGroupJsonSchemas,
           ...singleCustomGroupJsonSchema,
         },
         description: 'Custom group.',
diff --git a/utils/get-custom-groups-compare-options.ts b/utils/get-custom-groups-compare-options.ts
index b56fa8a..f8097cf 100644
--- a/utils/get-custom-groups-compare-options.ts
+++ b/utils/get-custom-groups-compare-options.ts
@@ -3,32 +3,27 @@ import type {
   CustomGroupsOption,
   GroupsOptions,
   CommonOptions,
-  TypeOption,
 } from '../types/common-options'
-import type { SortingNode } from '../types/sorting-node'
-import type { CompareOptions } from './compare'
 
-type Options = {
+type BaseOptions = {
   customGroups: DeprecatedCustomGroupsOption | CustomGroupsOption
   groups: GroupsOptions<string>
-  type: TypeOption
 } & CommonOptions
 
 /**
  * Retrieves the compare options used to sort a given group. If the group is a
  * custom group, its options will be favored over the default options. Returns
  * `null` if the group should not be sorted.
- * @param {Options} options - The sorting options,
- * including groups and custom groups.
+ * @param {BaseOptions} options - The sorting options, including groups and
+ * custom groups.
  * @param {number} groupNumber - The index of the group to retrieve compare
  * options for.
- * @returns {CompareOptions | null} The compare options for the group, or `null`
- * if the group should not be sorted.
+ * @returns {BaseOptions} The options for the group
  */
-export let getCustomGroupsCompareOptions = <T extends SortingNode>(
+export let getCustomGroupsCompareOptions = <Options extends BaseOptions>(
   options: Options,
   groupNumber: number,
-): CompareOptions<T> | null => {
+): Options => {
   if (!Array.isArray(options.customGroups)) {
     return options
   }
diff --git a/utils/sort-nodes-by-groups.ts b/utils/sort-nodes-by-groups.ts
index 69b122a..f94779d 100644
--- a/utils/sort-nodes-by-groups.ts
+++ b/utils/sort-nodes-by-groups.ts
@@ -5,24 +5,29 @@ import type { CompareOptions } from './compare'
 import { getGroupNumber } from './get-group-number'
 import { sortNodes } from './sort-nodes'
 
-interface ExtraOptions<T extends SortingNode> {
-  /**
-   * If not provided, `options` will be used. If function returns null, nodes
-   * will not be sorted within the group.
-   */
-  getGroupCompareOptions?(groupNumber: number): CompareOptions<T> | null
+interface ExtraOptions<T extends SortingNode, Options extends BaseOptions<T>> {
+  getGroupCompareOptions?(groupNumber: number): CompareOptions<T> & Options
+
+  isNodeIgnoredForGroup?(node: T, compareOptions: Options): boolean
+
   ignoreEslintDisabledNodes: boolean
+
   isNodeIgnored?(node: T): boolean
 }
 
+type BaseOptions<T extends SortingNode> = CompareOptions<T> & GroupsOption
+
 interface GroupsOption {
   groups: GroupsOptions<string>
 }
 
-export let sortNodesByGroups = <T extends SortingNode>(
+export let sortNodesByGroups = <
+  T extends SortingNode,
+  Options extends BaseOptions<T>,
+>(
   nodes: T[],
-  options: CompareOptions<T> & GroupsOption,
-  extraOptions?: ExtraOptions<T>,
+  options: Options,
+  extraOptions?: ExtraOptions<T, Options>,
 ): T[] => {
   let nodesByNonIgnoredGroupNumber: Record<number, T[]> = {}
   let ignoredNodeIndices: number[] = []
@@ -44,15 +49,22 @@ export let sortNodesByGroups = <T extends SortingNode>(
   for (let groupNumber of Object.keys(nodesByNonIgnoredGroupNumber).sort(
     (a, b) => Number(a) - Number(b),
   )) {
-    let compareOptions = extraOptions?.getGroupCompareOptions
-      ? extraOptions.getGroupCompareOptions(Number(groupNumber))
-      : options
+    let compareOptions =
+      extraOptions?.getGroupCompareOptions?.(Number(groupNumber)) ?? options
     let nodesToPush = nodesByNonIgnoredGroupNumber[Number(groupNumber)]!
-    if (!compareOptions) {
-      sortedNodes.push(...nodesToPush)
-      continue
-    }
-    sortedNodes.push(...sortNodes(nodesToPush, compareOptions))
+
+    let groupIgnoredNodes = new Set(
+      nodesToPush.filter(node =>
+        extraOptions?.isNodeIgnoredForGroup?.(node, compareOptions),
+      ),
+    )
+
+    sortedNodes.push(
+      ...sortNodes(nodesToPush, compareOptions, {
+        isNodeIgnored: node => groupIgnoredNodes.has(node),
+        ignoreEslintDisabledNodes: false,
+      }),
+    )
   }
 
   // Add ignored nodes at the same position as they were before linting.
diff --git a/utils/sort-nodes.ts b/utils/sort-nodes.ts
index 25b3399..17aadbe 100644
--- a/utils/sort-nodes.ts
+++ b/utils/sort-nodes.ts
@@ -3,21 +3,24 @@ import type { CompareOptions } from './compare'
 
 import { compare } from './compare'
 
-interface ExtraOptions {
+interface ExtraOptions<T extends SortingNode> {
   ignoreEslintDisabledNodes: boolean
+
+  isNodeIgnored?(node: T): boolean
 }
 
 export let sortNodes = <T extends SortingNode>(
   nodes: T[],
   options: CompareOptions<T>,
-  extraOptions?: ExtraOptions,
+  extraOptions?: ExtraOptions<T>,
 ): T[] => {
   let nonIgnoredNodes: T[] = []
   let ignoredNodeIndices: number[] = []
   for (let [index, sortingNode] of nodes.entries()) {
     if (
-      sortingNode.isEslintDisabled &&
-      extraOptions?.ignoreEslintDisabledNodes
+      (sortingNode.isEslintDisabled &&
+        extraOptions?.ignoreEslintDisabledNodes) ||
+      extraOptions?.isNodeIgnored?.(sortingNode)
     ) {
       ignoredNodeIndices.push(index)
     } else {


Test patch:
diff --git a/test/rules/sort-interfaces.test.ts b/test/rules/sort-interfaces.test.ts
index 347157c..96adf71 100644
--- a/test/rules/sort-interfaces.test.ts
+++ b/test/rules/sort-interfaces.test.ts
@@ -1110,6 +1110,65 @@ describe(ruleName, () => {
         })
       }
 
+      for (let dateElementValuePattern of [
+        'Date',
+        ['noMatch', 'Date'],
+        { pattern: 'DATE', flags: 'i' },
+        ['noMatch', { pattern: 'DATE', flags: 'i' }],
+      ]) {
+        ruleTester.run(`${ruleName}: filters on elementValuePattern`, rule, {
+          invalid: [
+            {
+              options: [
+                {
+                  customGroups: [
+                    {
+                      elementValuePattern: dateElementValuePattern,
+                      groupName: 'date',
+                    },
+                    {
+                      elementValuePattern: 'number',
+                      groupName: 'number',
+                    },
+                  ],
+                  groups: ['number', 'date', 'unknown'],
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'number',
+                    leftGroup: 'date',
+                    right: 'z',
+                    left: 'y',
+                  },
+                  messageId: 'unexpectedInterfacePropertiesGroupOrder',
+                },
+              ],
+              output: dedent`
+                interface Interface {
+                  a: number
+                  z: number
+                  b: Date
+                  y: Date
+                  c(): string
+                }
+              `,
+              code: dedent`
+                interface Interface {
+                  a: number
+                  b: Date
+                  y: Date
+                  z: number
+                  c(): string
+                }
+              `,
+            },
+          ],
+          valid: [],
+        })
+      }
+
       ruleTester.run(
         `${ruleName}: sort custom groups by overriding 'type' and 'order'`,
         rule,
@@ -1197,6 +1256,79 @@ describe(ruleName, () => {
         },
       )
 
+      ruleTester.run(
+        `${ruleName}: sort custom groups by overriding 'sortBy'`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'fooElementsSortedByValue',
+                    leftGroup: 'unknown',
+                    right: 'fooC',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedInterfacePropertiesGroupOrder',
+                },
+                {
+                  data: {
+                    right: 'fooA',
+                    left: 'fooB',
+                  },
+                  messageId: 'unexpectedInterfacePropertiesOrder',
+                },
+                {
+                  data: {
+                    rightGroup: 'fooElementsSortedByValue',
+                    leftGroup: 'unknown',
+                    right: 'fooMethod',
+                    left: 'a',
+                  },
+                  messageId: 'unexpectedInterfacePropertiesGroupOrder',
+                },
+              ],
+              options: [
+                {
+                  customGroups: [
+                    {
+                      groupName: 'fooElementsSortedByValue',
+                      elementNamePattern: '^foo',
+                      sortBy: 'value',
+                    },
+                  ],
+                  groups: ['fooElementsSortedByValue', 'unknown'],
+                  type: 'alphabetical',
+                  order: 'asc',
+                },
+              ],
+              output: dedent`
+                interface Interface {
+                  fooA: Date
+                  fooC: number
+                  fooB: string
+                  fooMethod(): void
+                  a: string
+                  z: boolean
+                }
+              `,
+              code: dedent`
+                interface Interface {
+                  z: boolean
+                  fooC: number
+                  fooB: string
+                  fooA: Date
+                  a: string
+                  fooMethod(): void
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
       ruleTester.run(
         `${ruleName}: does not sort custom groups with 'unsorted' type`,
         rule,
@@ -2795,6 +2927,173 @@ describe(ruleName, () => {
         )
       })
     })
+
+    describe(`${ruleName}(${type}): sorting by value`, () => {
+      ruleTester.run(`${ruleName}(${type}): allows sorting by value`, rule, {
+        invalid: [
+          {
+            errors: [
+              {
+                data: {
+                  right: 'b',
+                  left: 'a',
+                },
+                messageId: 'unexpectedInterfacePropertiesOrder',
+              },
+            ],
+            output: dedent`
+              interface Interface {
+                b: 'a'
+                a: 'b'
+              }
+            `,
+            code: dedent`
+              interface Interface {
+                a: 'b'
+                b: 'a'
+              }
+            `,
+            options: [
+              {
+                sortBy: 'value',
+                ...options,
+              },
+            ],
+          },
+        ],
+        valid: [],
+      })
+
+      ruleTester.run(
+        `${ruleName}(${type}): does not enforce sorting of non-properties in the same group`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'a',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedInterfacePropertiesOrder',
+                },
+                {
+                  data: {
+                    right: 'y',
+                    left: 'a',
+                  },
+                  messageId: 'unexpectedInterfacePropertiesOrder',
+                },
+              ],
+              output: dedent`
+                interface Interface {
+                  y: 'y'
+                  a(): void
+                  z: 'z'
+                }
+              `,
+              code: dedent`
+                interface Interface {
+                  z: 'z'
+                  a(): void
+                  y: 'y'
+                }
+              `,
+              options: [
+                {
+                  sortBy: 'value',
+                  ...options,
+                },
+              ],
+            },
+            {
+              errors: [
+                {
+                  data: {
+                    right: '[key: string]',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedInterfacePropertiesOrder',
+                },
+                {
+                  data: {
+                    left: '[key: string]',
+                    right: 'y',
+                  },
+                  messageId: 'unexpectedInterfacePropertiesOrder',
+                },
+              ],
+              output: dedent`
+                interface Interface {
+                  y: 'y'
+                  [key: string]
+                  z: 'z'
+                }
+              `,
+              code: dedent`
+                interface Interface {
+                  z: 'z'
+                  [key: string]
+                  y: 'y'
+                }
+              `,
+              options: [
+                {
+                  sortBy: 'value',
+                  ...options,
+                },
+              ],
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): enforces grouping but does not enforce sorting of non-properties`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'method',
+                    leftGroup: 'unknown',
+                    right: 'a',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedInterfacePropertiesGroupOrder',
+                },
+              ],
+              options: [
+                {
+                  sortBy: 'value',
+                  ...options,
+                  groups: ['method', 'unknown'],
+                },
+              ],
+              output: dedent`
+                interface Interface {
+                  b(): void
+                  a(): void
+                  z: 'z'
+                }
+              `,
+              code: dedent`
+                interface Interface {
+                  b(): void
+                  z: 'z'
+                  a(): void
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+    })
   })
 
   describe(`${ruleName}: sorting by natural order`, () => {
diff --git a/test/rules/sort-object-types.test.ts b/test/rules/sort-object-types.test.ts
index 27ead78..b49351a 100644
--- a/test/rules/sort-object-types.test.ts
+++ b/test/rules/sort-object-types.test.ts
@@ -851,6 +851,65 @@ describe(ruleName, () => {
         })
       }
 
+      for (let dateElementValuePattern of [
+        'Date',
+        ['noMatch', 'Date'],
+        { pattern: 'DATE', flags: 'i' },
+        ['noMatch', { pattern: 'DATE', flags: 'i' }],
+      ]) {
+        ruleTester.run(`${ruleName}: filters on elementValuePattern`, rule, {
+          invalid: [
+            {
+              options: [
+                {
+                  customGroups: [
+                    {
+                      elementValuePattern: dateElementValuePattern,
+                      groupName: 'date',
+                    },
+                    {
+                      elementValuePattern: 'number',
+                      groupName: 'number',
+                    },
+                  ],
+                  groups: ['number', 'date', 'unknown'],
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'number',
+                    leftGroup: 'date',
+                    right: 'z',
+                    left: 'y',
+                  },
+                  messageId: 'unexpectedObjectTypesGroupOrder',
+                },
+              ],
+              output: dedent`
+                type Type = {
+                  a: number
+                  z: number
+                  b: Date
+                  y: Date
+                  c(): string
+                }
+              `,
+              code: dedent`
+                type Type = {
+                  a: number
+                  b: Date
+                  y: Date
+                  z: number
+                  c(): string
+                }
+              `,
+            },
+          ],
+          valid: [],
+        })
+      }
+
       ruleTester.run(
         `${ruleName}: sort custom groups by overriding 'type' and 'order'`,
         rule,
@@ -938,6 +997,79 @@ describe(ruleName, () => {
         },
       )
 
+      ruleTester.run(
+        `${ruleName}: sort custom groups by overriding 'sortBy'`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'fooElementsSortedByValue',
+                    leftGroup: 'unknown',
+                    right: 'fooC',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedObjectTypesGroupOrder',
+                },
+                {
+                  data: {
+                    right: 'fooA',
+                    left: 'fooB',
+                  },
+                  messageId: 'unexpectedObjectTypesOrder',
+                },
+                {
+                  data: {
+                    rightGroup: 'fooElementsSortedByValue',
+                    leftGroup: 'unknown',
+                    right: 'fooMethod',
+                    left: 'a',
+                  },
+                  messageId: 'unexpectedObjectTypesGroupOrder',
+                },
+              ],
+              options: [
+                {
+                  customGroups: [
+                    {
+                      groupName: 'fooElementsSortedByValue',
+                      elementNamePattern: '^foo',
+                      sortBy: 'value',
+                    },
+                  ],
+                  groups: ['fooElementsSortedByValue', 'unknown'],
+                  type: 'alphabetical',
+                  order: 'asc',
+                },
+              ],
+              output: dedent`
+                type Type = {
+                  fooA: Date
+                  fooC: number
+                  fooB: string
+                  fooMethod(): void
+                  a: string
+                  z: boolean
+                }
+              `,
+              code: dedent`
+                type Type = {
+                  z: boolean
+                  fooC: number
+                  fooB: string
+                  fooA: Date
+                  a: string
+                  fooMethod(): void
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
       ruleTester.run(
         `${ruleName}: does not sort custom groups with 'unsorted' type`,
         rule,
@@ -2688,6 +2820,173 @@ describe(ruleName, () => {
         )
       })
     })
+
+    describe(`${ruleName}(${type}): sorting by value`, () => {
+      ruleTester.run(`${ruleName}(${type}): allows sorting by value`, rule, {
+        invalid: [
+          {
+            errors: [
+              {
+                data: {
+                  right: 'b',
+                  left: 'a',
+                },
+                messageId: 'unexpectedObjectTypesOrder',
+              },
+            ],
+            output: dedent`
+              type Type = {
+                b: 'a'
+                a: 'b'
+              }
+            `,
+            options: [
+              {
+                sortBy: 'value',
+                ...options,
+              },
+            ],
+            code: dedent`
+              type Type = {
+                a: 'b'
+                b: 'a'
+              }
+            `,
+          },
+        ],
+        valid: [],
+      })
+
+      ruleTester.run(
+        `${ruleName}(${type}): does not enforce sorting of non-properties in the same group`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'a',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedObjectTypesOrder',
+                },
+                {
+                  data: {
+                    right: 'y',
+                    left: 'a',
+                  },
+                  messageId: 'unexpectedObjectTypesOrder',
+                },
+              ],
+              output: dedent`
+                type Type = {
+                  y: 'y'
+                  a(): void
+                  z: 'z'
+                }
+              `,
+              code: dedent`
+                type Type = {
+                  z: 'z'
+                  a(): void
+                  y: 'y'
+                }
+              `,
+              options: [
+                {
+                  sortBy: 'value',
+                  ...options,
+                },
+              ],
+            },
+            {
+              errors: [
+                {
+                  data: {
+                    right: '[key: string]',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedObjectTypesOrder',
+                },
+                {
+                  data: {
+                    left: '[key: string]',
+                    right: 'y',
+                  },
+                  messageId: 'unexpectedObjectTypesOrder',
+                },
+              ],
+              output: dedent`
+                type Type = {
+                  y: 'y'
+                  [key: string]
+                  z: 'z'
+                }
+              `,
+              code: dedent`
+                type Type = {
+                  z: 'z'
+                  [key: string]
+                  y: 'y'
+                }
+              `,
+              options: [
+                {
+                  sortBy: 'value',
+                  ...options,
+                },
+              ],
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): enforces grouping but does not enforce sorting of non-properties`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'method',
+                    leftGroup: 'unknown',
+                    right: 'a',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedObjectTypesGroupOrder',
+                },
+              ],
+              options: [
+                {
+                  sortBy: 'value',
+                  ...options,
+                  groups: ['method', 'unknown'],
+                },
+              ],
+              output: dedent`
+                type Type = {
+                  b(): void
+                  a(): void
+                  z: 'z'
+                }
+              `,
+              code: dedent`
+                type Type = {
+                  b(): void
+                  z: 'z'
+                  a(): void
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+    })
   })
 
   describe(`${ruleName}: sorting by natural order`, () => {
diff --git a/test/rules/sort-object-types/build-node-value-getter.test.ts b/test/rules/sort-object-types/build-node-value-getter.test.ts
new file mode 100644
index 0000000..746ed77
--- /dev/null
+++ b/test/rules/sort-object-types/build-node-value-getter.test.ts
@@ -0,0 +1,33 @@
+import { describe, expect, it } from 'vitest'
+
+import type { SortObjectTypesSortingNode } from '../../../rules/sort-object-types/types'
+
+import { buildNodeValueGetter } from '../../../rules/sort-object-types/build-node-value-getter'
+
+describe('build-node-value-getter', () => {
+  it('returns null when "sortBy" is "name"', () => {
+    let nodeValueGetter = buildNodeValueGetter('name')
+    expect(nodeValueGetter).toBeNull()
+  })
+
+  describe('when "sortBy" is "value"', () => {
+    let nodeValueGetter = buildNodeValueGetter('value')!
+
+    it("nodeValueGetter should return the node's name if it exists", () => {
+      expect(nodeValueGetter(createTestNode({ value: 'value' }))).toBe('value')
+    })
+
+    it("nodeValueGetter should return an empty string if the node's name is null", () => {
+      expect(nodeValueGetter(createTestNode({ value: null }))).toBe('')
+    })
+  })
+
+  let createTestNode = ({
+    value,
+  }: {
+    value: string | null
+  }): SortObjectTypesSortingNode =>
+    ({
+      value,
+    }) as SortObjectTypesSortingNode
+})
diff --git a/test/utils/sort-nodes-by-groups.test.ts b/test/utils/sort-nodes-by-groups.test.ts
new file mode 100644
index 0000000..60cd488
--- /dev/null
+++ b/test/utils/sort-nodes-by-groups.test.ts
@@ -0,0 +1,122 @@
+import { describe, expect, it } from 'vitest'
+
+import type { SortingNode } from '../../types/sorting-node'
+
+import { sortNodesByGroups } from '../../utils/sort-nodes-by-groups'
+
+describe('sort-nodes-by-groups', () => {
+  let compareOptions = {
+    fallbackSort: { type: 'unsorted' },
+    specialCharacters: 'keep',
+    type: 'alphabetical',
+    ignoreCase: false,
+    locales: 'en-US',
+    order: 'asc',
+  } as const
+
+  it('sorts nodes by groups', () => {
+    let nodeA = createTestNode({ group: 'group2', name: 'a' })
+    let nodeB = createTestNode({ group: 'group1', name: 'b' })
+    let nodeC = createTestNode({ group: 'group2', name: 'c' })
+    let nodeD = createTestNode({ group: 'group1', name: 'd' })
+    expect(
+      sortNodesByGroups([nodeD, nodeC, nodeB, nodeA], {
+        ...compareOptions,
+        groups: ['group1', 'group2'],
+      }),
+    ).toStrictEqual([nodeB, nodeD, nodeA, nodeC])
+  })
+
+  describe('ignoreEslintDisabledNodes', () => {
+    let nodeA = createTestNode({ group: 'group2', name: 'a' })
+    let nodeB = createTestNode({ group: 'group1', name: 'b' })
+    let nodeC = createTestNode({
+      isEslintDisabled: true,
+      name: 'group2',
+      group: 'c',
+    })
+    let nodeD = createTestNode({ group: 'group1', name: 'd' })
+
+    it('should ignore eslint disabled nodes if "ignoreEslintDisabledNodes" is true', () => {
+      expect(
+        sortNodesByGroups(
+          [nodeD, nodeC, nodeB, nodeA],
+          {
+            ...compareOptions,
+            groups: ['group1', 'group2'],
+          },
+          {
+            ignoreEslintDisabledNodes: true,
+          },
+        ),
+      ).toStrictEqual([nodeB, nodeC, nodeD, nodeA])
+    })
+
+    it('should not ignore eslint disabled nodes if "ignoreEslintDisabledNodes" is false', () => {
+      expect(
+        sortNodesByGroups(
+          [nodeD, nodeC, nodeB, nodeA],
+          {
+            ...compareOptions,
+            groups: ['group1', 'group2'],
+          },
+          {
+            ignoreEslintDisabledNodes: false,
+          },
+        ),
+      ).toStrictEqual([nodeB, nodeD, nodeA, nodeC])
+    })
+  })
+
+  describe('isNodeIgnored', () => {
+    it('should ignore a node if "isNodeIgnored(node)" is true', () => {
+      let nodeA = createTestNode({ group: 'group2', name: 'a' })
+      let nodeB = createTestNode({ group: 'group1', name: 'b' })
+      let nodeC = createTestNode({ group: 'group2', name: 'c' })
+      let nodeD = createTestNode({ group: 'group1', name: 'd' })
+      expect(
+        sortNodesByGroups(
+          [nodeD, nodeC, nodeB, nodeA],
+          {
+            ...compareOptions,
+            groups: ['group1', 'group2'],
+          },
+          {
+            isNodeIgnored: node => node === nodeC,
+            ignoreEslintDisabledNodes: false,
+          },
+        ),
+      ).toStrictEqual([nodeB, nodeC, nodeD, nodeA])
+    })
+  })
+
+  describe('isNodeIgnoredForGroup', () => {
+    it('should ignore a node for a given group if "isNodeIgnoredForGroup(node, ...)" is true', () => {
+      let nodeA = createTestNode({ group: 'group2', name: 'a' })
+      let nodeB = createTestNode({ group: 'group1', name: 'b' })
+      let nodeC = createTestNode({ group: 'group2', name: 'c' })
+      let nodeD = createTestNode({ group: 'group1', name: 'd' })
+      let nodeE = createTestNode({ group: 'group2', name: 'e' })
+      let nodeF = createTestNode({ group: 'group1', name: 'f' })
+      expect(
+        sortNodesByGroups(
+          [nodeF, nodeE, nodeD, nodeC, nodeB, nodeA],
+          {
+            ...compareOptions,
+            groups: ['group1', 'group2'],
+          },
+          {
+            isNodeIgnoredForGroup: node => node === nodeB,
+            ignoreEslintDisabledNodes: false,
+          },
+        ),
+      ).toStrictEqual([nodeD, nodeF, nodeB, nodeA, nodeC, nodeE])
+    })
+  })
+
+  let createTestNode = (node: {
+    isEslintDisabled?: boolean
+    group?: string
+    name: string
+  }): SortingNode => node as SortingNode
+})
diff --git a/test/utils/sort-nodes.test.ts b/test/utils/sort-nodes.test.ts
new file mode 100644
index 0000000..c08d7fa
--- /dev/null
+++ b/test/utils/sort-nodes.test.ts
@@ -0,0 +1,63 @@
+import { describe, expect, it } from 'vitest'
+
+import type { SortingNode } from '../../types/sorting-node'
+
+import { sortNodes } from '../../utils/sort-nodes'
+
+describe('sort-nodes', () => {
+  let compareOptions = {
+    fallbackSort: { type: 'unsorted' },
+    specialCharacters: 'keep',
+    type: 'alphabetical',
+    ignoreCase: false,
+    locales: 'en-US',
+    order: 'asc',
+  } as const
+
+  it('sorts nodes', () => {
+    let a = createTestNode({ name: 'a' })
+    let b = createTestNode({ name: 'b' })
+    expect(sortNodes([b, a], compareOptions)).toStrictEqual([a, b])
+  })
+
+  describe('ignoreEslintDisabledNodes', () => {
+    let nodeA = createTestNode({ name: 'a' })
+    let nodeB = createTestNode({ name: 'b' })
+    let nodeC = createTestNode({ isEslintDisabled: true, name: 'c' })
+
+    it('should ignore eslint disabled nodes if "ignoreEslintDisabledNodes" is true', () => {
+      expect(
+        sortNodes([nodeB, nodeC, nodeA], compareOptions, {
+          ignoreEslintDisabledNodes: true,
+        }),
+      ).toStrictEqual([nodeA, nodeC, nodeB])
+    })
+
+    it('should not ignore eslint disabled nodes if "ignoreEslintDisabledNodes" is false', () => {
+      expect(
+        sortNodes([nodeB, nodeC, nodeA], compareOptions, {
+          ignoreEslintDisabledNodes: false,
+        }),
+      ).toStrictEqual([nodeA, nodeB, nodeC])
+    })
+  })
+
+  describe('isNodeIgnored', () => {
+    it('should ignore a node if "isNodeIgnored(node)" is true', () => {
+      let nodeA = createTestNode({ name: 'a' })
+      let nodeB = createTestNode({ name: 'b' })
+      let nodeC = createTestNode({ name: 'c' })
+      expect(
+        sortNodes([nodeB, nodeC, nodeA], compareOptions, {
+          isNodeIgnored: node => node === nodeC,
+          ignoreEslintDisabledNodes: false,
+        }),
+      ).toStrictEqual([nodeA, nodeC, nodeB])
+    })
+  })
+
+  let createTestNode = (node: {
+    isEslintDisabled?: boolean
+    name: string
+  }): SortingNode => node as SortingNode
+})