diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index a9800953..8a40909d 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -258,6 +258,10 @@ export default defineConfig({
text: 'sort-named-imports',
link: '/rules/sort-named-imports',
},
+ {
+ text: 'sort-object-types',
+ link: '/rules/sort-object-types',
+ },
{
text: 'sort-objects',
link: '/rules/sort-objects',
diff --git a/docs/rules/index.md b/docs/rules/index.md
index feaf6b99..96175817 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -18,6 +18,7 @@ title: Rules
| [sort-map-elements](/rules/sort-map-elements) | enforce sorted Map elements | 🔧 |
| [sort-named-exports](/rules/sort-named-exports) | enforce sorted named exports | 🔧 |
| [sort-named-imports](/rules/sort-named-imports) | enforce sorted named imports | 🔧 |
+| [sort-object-types](/rules/sort-object-types) | enforce sorted object types | 🔧 |
| [sort-objects](/rules/sort-objects) | enforce sorted objects | 🔧 |
| [sort-union-types](/rules/sort-union-types) | enforce sorted union types | 🔧 |
diff --git a/docs/rules/sort-object-types.md b/docs/rules/sort-object-types.md
new file mode 100644
index 00000000..9a92561b
--- /dev/null
+++ b/docs/rules/sort-object-types.md
@@ -0,0 +1,146 @@
+---
+title: sort-object-types
+---
+
+# sort-object-types
+
+💼 This rule is enabled in the following [configs](/configs/): `recommended-alphabetical`, `recommended-line-length`, `recommended-natural`.
+
+🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+
+
+
+## 📖 Rule Details
+
+Enforce sorted object types.
+
+This rule standardizes the order of members of an object type in a TypeScript. The order in which the members are defined within an object type does not affect the type system or the behavior of the code.
+
+## 💡 Examples
+
+### Alphabetical and Natural Sorting
+
+
+```ts
+// Incorrect
+type User = {
+ name: string
+ email: string
+ role: Role
+ isAdmin: boolean
+}
+
+// Correct
+type User = {
+ email: string
+ isAdmin: boolean
+ name: string
+ role: Role
+}
+```
+
+### Sorting by Line Length
+
+
+```ts
+// Incorrect
+type User = {
+ name: string
+ email: string
+ role: Role
+ isAdmin: boolean
+}
+
+// Correct
+type User = {
+ isAdmin: boolean
+ email: string
+ name: string
+ role: Role
+}
+```
+
+## 🔧 Options
+
+This rule accepts an options object with the following properties:
+
+```ts
+interface Options {
+ type?: 'alphabetical' | 'natural' | 'natural'
+ order?: 'asc' | 'desc'
+ 'ignore-case'?: boolean
+}
+```
+
+### type
+
+(default: `'alphabetical'`)
+
+- `alphabetical` - sort alphabetically.
+- `natural` - sort in natural order.
+- `line-length` - sort by code line length.
+
+### order
+
+(default: `'asc'`)
+
+- `asc` - enforce properties to be in ascending order.
+- `desc` - enforce properties to be in descending order.
+
+### ignore-case
+
+(default: `false`)
+
+Only affects alphabetical and natural sorting. When `true` the rule ignores the case-sensitivity of the order.
+
+## ⚙️ Usage
+
+### Legacy Config
+
+```json
+// .eslintrc
+{
+ "rules": {
+ "perfectionist/sort-object-types": [
+ "error",
+ {
+ "type": "line-length",
+ "order": "desc"
+ }
+ ]
+ }
+}
+```
+
+### Flat Config
+
+```js
+// eslint.config.js
+import perfectionist from 'eslint-plugin-perfectionist'
+
+export default [
+ {
+ plugins: {
+ perfectionist,
+ },
+ rules: {
+ 'perfectionist/sort-object-types': [
+ 'error',
+ {
+ type: 'line-length',
+ order: 'desc',
+ },
+ ],
+ },
+ },
+]
+```
+
+## 🚀 Version
+
+Coming soon.
+
+## 📚 Resources
+
+- [Rule source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/rules/sort-object-types.ts)
+- [Test source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/test/sort-object-types.test.ts)
diff --git a/index.ts b/index.ts
index 4bcd2246..54b46d50 100644
--- a/index.ts
+++ b/index.ts
@@ -6,6 +6,7 @@ import sortJsxProps, { RULE_NAME as sortJsxPropsName } from './rules/sort-jsx-pr
import sortMapElements, { RULE_NAME as sortMapElementsName } from './rules/sort-map-elements'
import sortNamedExports, { RULE_NAME as sortNamedExportsName } from './rules/sort-named-exports'
import sortNamedImports, { RULE_NAME as sortNamedImportsName } from './rules/sort-named-imports'
+import sortObjectTypes, { RULE_NAME as sortObjectTypesName } from './rules/sort-object-types'
import sortObjects, { RULE_NAME as sortObjectsName } from './rules/sort-objects'
import sortUnionTypes, { RULE_NAME as sortUnionTypesName } from './rules/sort-union-types'
import { SortType, SortOrder } from './typings'
@@ -66,6 +67,7 @@ let createConfigWithOptions = (options: {
[sortMapElementsName]: ['error'],
[sortNamedExportsName]: ['error'],
[sortNamedImportsName]: ['error'],
+ [sortObjectTypesName]: ['error'],
[sortObjectsName]: [
'error',
{
@@ -96,6 +98,7 @@ export default {
[sortMapElementsName]: sortMapElements,
[sortNamedExportsName]: sortNamedExports,
[sortNamedImportsName]: sortNamedImports,
+ [sortObjectTypesName]: sortObjectTypes,
[sortObjectsName]: sortObjects,
[sortUnionTypesName]: sortUnionTypes,
},
diff --git a/readme.md b/readme.md
index 7b16f5ec..3a316b02 100644
--- a/readme.md
+++ b/readme.md
@@ -135,6 +135,7 @@ export default [perfectionistPluginRecommendedLineLength]
| [sort-map-elements](https://eslint-plugin-perfectionist.azat.io/rules/sort-map-elements) | enforce sorted Map elements | 🔧 |
| [sort-named-exports](https://eslint-plugin-perfectionist.azat.io/rules/sort-named-exports) | enforce sorted named exports | 🔧 |
| [sort-named-imports](https://eslint-plugin-perfectionist.azat.io/rules/sort-named-imports) | enforce sorted named imports | 🔧 |
+| [sort-object-types](https://eslint-plugin-perfectionist.azat.io/rules/sort-object-types) | enforce sorted object types | 🔧 |
| [sort-objects](https://eslint-plugin-perfectionist.azat.io/rules/sort-objects) | enforce sorted objects | 🔧 |
| [sort-union-types](https://eslint-plugin-perfectionist.azat.io/rules/sort-union-types) | enforce sorted union types | 🔧 |
diff --git a/rules/sort-object-types.ts b/rules/sort-object-types.ts
new file mode 100644
index 00000000..903ba706
--- /dev/null
+++ b/rules/sort-object-types.ts
@@ -0,0 +1,129 @@
+import type { SortingNode } from '../typings'
+
+import { AST_NODE_TYPES } from '@typescript-eslint/types'
+
+import { createEslintRule } from '../utils/create-eslint-rule'
+import { toSingleLine } from '../utils/to-single-line'
+import { rangeToDiff } from '../utils/range-to-diff'
+import { SortType, SortOrder } from '../typings'
+import { sortNodes } from '../utils/sort-nodes'
+import { makeFixes } from '../utils/make-fixes'
+import { complete } from '../utils/complete'
+import { pairwise } from '../utils/pairwise'
+import { compare } from '../utils/compare'
+
+type MESSAGE_ID = 'unexpectedObjectTypesOrder'
+
+type Options = [
+ Partial<{
+ 'ignore-case': boolean
+ order: SortOrder
+ type: SortType
+ }>,
+]
+
+export const RULE_NAME = 'sort-object-types'
+
+export default createEslintRule({
+ name: RULE_NAME,
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'enforce sorted object types',
+ recommended: false,
+ },
+ fixable: 'code',
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ type: {
+ enum: [
+ SortType.alphabetical,
+ SortType.natural,
+ SortType['line-length'],
+ ],
+ default: SortType.natural,
+ },
+ order: {
+ enum: [SortOrder.asc, SortOrder.desc],
+ default: SortOrder.asc,
+ },
+ 'ignore-case': {
+ type: 'boolean',
+ default: false,
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
+ messages: {
+ unexpectedObjectTypesOrder:
+ 'Expected "{{second}}" to come before "{{first}}"',
+ },
+ },
+ defaultOptions: [
+ {
+ type: SortType.alphabetical,
+ order: SortOrder.asc,
+ },
+ ],
+ create: context => ({
+ TSTypeLiteral: node => {
+ if (node.members.length > 1) {
+ let options = complete(context.options.at(0), {
+ type: SortType.alphabetical,
+ 'ignore-case': false,
+ order: SortOrder.asc,
+ })
+
+ let source = context.getSourceCode()
+
+ let nodes: SortingNode[] = node.members.map(member => {
+ let name: string
+
+ if (member.type === AST_NODE_TYPES.TSPropertySignature) {
+ if (member.key.type === AST_NODE_TYPES.Identifier) {
+ ;({ name } = member.key)
+ } else if (member.key.type === AST_NODE_TYPES.Literal) {
+ name = `${member.key.value}`
+ } else {
+ name = source.text.slice(
+ member.range.at(0),
+ member.typeAnnotation?.range.at(0),
+ )
+ }
+ } else if (member.type === AST_NODE_TYPES.TSIndexSignature) {
+ let endIndex: number =
+ member.typeAnnotation?.range.at(0) ?? member.range.at(1)!
+
+ name = source.text.slice(member.range.at(0), endIndex)
+ } else {
+ name = source.text.slice(member.range.at(0), member.range.at(1))
+ }
+
+ return {
+ size: rangeToDiff(member.range),
+ node: member,
+ name,
+ }
+ })
+
+ pairwise(nodes, (first, second) => {
+ if (compare(first, second, options)) {
+ context.report({
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: toSingleLine(first.name),
+ second: toSingleLine(second.name),
+ },
+ node: second.node,
+ fix: fixer =>
+ makeFixes(fixer, nodes, sortNodes(nodes, options), source),
+ })
+ }
+ })
+ }
+ },
+ }),
+})
diff --git a/test/sort-object-types.test.ts b/test/sort-object-types.test.ts
new file mode 100644
index 00000000..49af446b
--- /dev/null
+++ b/test/sort-object-types.test.ts
@@ -0,0 +1,812 @@
+import { ESLintUtils } from '@typescript-eslint/utils'
+import { describe, it } from 'vitest'
+import { dedent } from 'ts-dedent'
+
+import rule, { RULE_NAME } from '../rules/sort-object-types'
+import { SortType, SortOrder } from '../typings'
+
+describe(RULE_NAME, () => {
+ let ruleTester = new ESLintUtils.RuleTester({
+ parser: '@typescript-eslint/parser',
+ })
+
+ describe(`${RULE_NAME}: sorting by alphabetical order`, () => {
+ let type = 'alphabetical-order'
+
+ it(`${RULE_NAME}(${type}): sorts type members`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ type Mushishi = {
+ birthname: 'Yoki'
+ name: 'Ginko'
+ status: 'wanderer'
+ }
+ `,
+ options: [
+ {
+ type: SortType.alphabetical,
+ order: SortOrder.asc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ type Mushishi = {
+ name: 'Ginko'
+ birthname: 'Yoki'
+ status: 'wanderer'
+ }
+ `,
+ output: dedent`
+ type Mushishi = {
+ birthname: 'Yoki'
+ name: 'Ginko'
+ status: 'wanderer'
+ }
+ `,
+ options: [
+ {
+ type: SortType.alphabetical,
+ order: SortOrder.asc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'name',
+ second: 'birthname',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ it(`${RULE_NAME}(${type}): sorts type members in function args`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ let handleDemonSlayerAttack = (attack: {
+ attackType: string
+ demon: string
+ slayerName: string
+ }) => {
+ // ...
+ }
+ `,
+ options: [
+ {
+ type: SortType.alphabetical,
+ order: SortOrder.asc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ let handleDemonSlayerAttack = (attack: {
+ slayerName: string
+ attackType: string
+ demon: string
+ }) => {
+ // ...
+ }
+ `,
+ output: dedent`
+ let handleDemonSlayerAttack = (attack: {
+ attackType: string
+ demon: string
+ slayerName: string
+ }) => {
+ // ...
+ }
+ `,
+ options: [
+ {
+ type: SortType.alphabetical,
+ order: SortOrder.asc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'slayerName',
+ second: 'attackType',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ it(`${RULE_NAME}(${type}): sorts type members with computed keys`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ type SquadMember = {
+ [key: string]: string
+ age?: 30
+ name: 'Levi Ackermann'
+ occupation: 'soldier'
+ rank: 'captain'
+ [residence]: 'Wall Rose'
+ }
+ `,
+ options: [
+ {
+ type: SortType.alphabetical,
+ order: SortOrder.asc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ type SquadMember = {
+ age?: 30
+ [key: string]: string
+ occupation: 'soldier'
+ name: 'Levi Ackermann'
+ [residence]: 'Wall Rose'
+ rank: 'captain'
+ }
+ `,
+ output: dedent`
+ type SquadMember = {
+ [key: string]: string
+ age?: 30
+ name: 'Levi Ackermann'
+ occupation: 'soldier'
+ rank: 'captain'
+ [residence]: 'Wall Rose'
+ }
+ `,
+ options: [
+ {
+ type: SortType.alphabetical,
+ order: SortOrder.asc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'age',
+ second: '[key: string]',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'occupation',
+ second: 'name',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'residence',
+ second: 'rank',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ it(`${RULE_NAME}(${type}): sorts type members with any key types`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ type ParanoiaAgent = {
+ [...kills]
+ [[data]]: string
+ [name in victims]?
+ [8]: Victim
+ goldenBatAttack(): void
+ hide?: () => void
+ }
+ `,
+ options: [
+ {
+ type: SortType.alphabetical,
+ order: SortOrder.asc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ type ParanoiaAgent = {
+ [...kills]
+ [[data]]: string
+ goldenBatAttack(): void
+ [8]: Victim
+ hide?: () => void
+ }
+ `,
+ output: dedent`
+ type ParanoiaAgent = {
+ [...kills]
+ [[data]]: string
+ [8]: Victim
+ goldenBatAttack(): void
+ hide?: () => void
+ }
+ `,
+ options: [
+ {
+ type: SortType.alphabetical,
+ order: SortOrder.asc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'goldenBatAttack(): void',
+ second: '8',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+ })
+
+ describe(`${RULE_NAME}: sorting by natural order`, () => {
+ let type = 'natural-order'
+
+ it(`${RULE_NAME}(${type}): sorts type members`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ type Mushishi = {
+ birthname: 'Yoki'
+ name: 'Ginko'
+ status: 'wanderer'
+ }
+ `,
+ options: [
+ {
+ type: SortType.natural,
+ order: SortOrder.asc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ type Mushishi = {
+ name: 'Ginko'
+ birthname: 'Yoki'
+ status: 'wanderer'
+ }
+ `,
+ output: dedent`
+ type Mushishi = {
+ birthname: 'Yoki'
+ name: 'Ginko'
+ status: 'wanderer'
+ }
+ `,
+ options: [
+ {
+ type: SortType.natural,
+ order: SortOrder.asc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'name',
+ second: 'birthname',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ it(`${RULE_NAME}(${type}): sorts type members in function args`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ let handleDemonSlayerAttack = (attack: {
+ attackType: string
+ demon: string
+ slayerName: string
+ }) => {
+ // ...
+ }
+ `,
+ options: [
+ {
+ type: SortType.natural,
+ order: SortOrder.asc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ let handleDemonSlayerAttack = (attack: {
+ slayerName: string
+ attackType: string
+ demon: string
+ }) => {
+ // ...
+ }
+ `,
+ output: dedent`
+ let handleDemonSlayerAttack = (attack: {
+ attackType: string
+ demon: string
+ slayerName: string
+ }) => {
+ // ...
+ }
+ `,
+ options: [
+ {
+ type: SortType.natural,
+ order: SortOrder.asc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'slayerName',
+ second: 'attackType',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ it(`${RULE_NAME}(${type}): sorts type members with computed keys`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ type SquadMember = {
+ [key: string]: string
+ age?: 30
+ name: 'Levi Ackermann'
+ occupation: 'soldier'
+ rank: 'captain'
+ [residence]: 'Wall Rose'
+ }
+ `,
+ options: [
+ {
+ type: SortType.natural,
+ order: SortOrder.asc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ type SquadMember = {
+ age?: 30
+ [key: string]: string
+ occupation: 'soldier'
+ name: 'Levi Ackermann'
+ [residence]: 'Wall Rose'
+ rank: 'captain'
+ }
+ `,
+ output: dedent`
+ type SquadMember = {
+ [key: string]: string
+ age?: 30
+ name: 'Levi Ackermann'
+ occupation: 'soldier'
+ rank: 'captain'
+ [residence]: 'Wall Rose'
+ }
+ `,
+ options: [
+ {
+ type: SortType.natural,
+ order: SortOrder.asc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'age',
+ second: '[key: string]',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'occupation',
+ second: 'name',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'residence',
+ second: 'rank',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ it(`${RULE_NAME}(${type}): sorts type members with any key types`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ type ParanoiaAgent = {
+ [...kills]
+ [[data]]: string
+ [name in victims]?
+ [8]: Victim
+ goldenBatAttack(): void
+ hide?: () => void
+ }
+ `,
+ options: [
+ {
+ type: SortType.natural,
+ order: SortOrder.asc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ type ParanoiaAgent = {
+ [...kills]
+ [[data]]: string
+ goldenBatAttack(): void
+ [8]: Victim
+ hide?: () => void
+ }
+ `,
+ output: dedent`
+ type ParanoiaAgent = {
+ [...kills]
+ [[data]]: string
+ [8]: Victim
+ goldenBatAttack(): void
+ hide?: () => void
+ }
+ `,
+ options: [
+ {
+ type: SortType.natural,
+ order: SortOrder.asc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'goldenBatAttack(): void',
+ second: '8',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+ })
+
+ describe(`${RULE_NAME}: sorting by line length`, () => {
+ let type = 'line-length-order'
+
+ it(`${RULE_NAME}(${type}): sorts type members`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ type Mushishi = {
+ status: 'wanderer'
+ birthname: 'Yoki'
+ name: 'Ginko'
+ }
+ `,
+ options: [
+ {
+ type: SortType['line-length'],
+ order: SortOrder.desc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ type Mushishi = {
+ name: 'Ginko'
+ birthname: 'Yoki'
+ status: 'wanderer'
+ }
+ `,
+ output: dedent`
+ type Mushishi = {
+ status: 'wanderer'
+ birthname: 'Yoki'
+ name: 'Ginko'
+ }
+ `,
+ options: [
+ {
+ type: SortType['line-length'],
+ order: SortOrder.desc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'name',
+ second: 'birthname',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'birthname',
+ second: 'status',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ it(`${RULE_NAME}(${type}): sorts type members in function args`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ let handleDemonSlayerAttack = (attack: {
+ attackType: string
+ slayerName: string
+ demon: string
+ }) => {
+ // ...
+ }
+ `,
+ options: [
+ {
+ type: SortType['line-length'],
+ order: SortOrder.desc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ let handleDemonSlayerAttack = (attack: {
+ slayerName: string
+ demon: string
+ attackType: string
+ }) => {
+ // ...
+ }
+ `,
+ output: dedent`
+ let handleDemonSlayerAttack = (attack: {
+ attackType: string
+ slayerName: string
+ demon: string
+ }) => {
+ // ...
+ }
+ `,
+ options: [
+ {
+ type: SortType['line-length'],
+ order: SortOrder.desc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'demon',
+ second: 'attackType',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ it(`${RULE_NAME}(${type}): sorts type members with computed keys`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ type SquadMember = {
+ [residence]: 'Wall Rose'
+ name: 'Levi Ackermann'
+ [key: string]: string
+ occupation: 'soldier'
+ rank: 'captain'
+ age?: 30
+ }
+ `,
+ options: [
+ {
+ type: SortType['line-length'],
+ order: SortOrder.desc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ type SquadMember = {
+ age?: 30
+ [key: string]: string
+ occupation: 'soldier'
+ name: 'Levi Ackermann'
+ [residence]: 'Wall Rose'
+ rank: 'captain'
+ }
+ `,
+ output: dedent`
+ type SquadMember = {
+ [residence]: 'Wall Rose'
+ name: 'Levi Ackermann'
+ occupation: 'soldier'
+ [key: string]: string
+ rank: 'captain'
+ age?: 30
+ }
+ `,
+ options: [
+ {
+ type: SortType['line-length'],
+ order: SortOrder.desc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'age',
+ second: '[key: string]',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'occupation',
+ second: 'name',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: 'name',
+ second: 'residence',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ it(`${RULE_NAME}(${type}): sorts type members with any key types`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ type ParanoiaAgent = {
+ goldenBatAttack(): void
+ hide?: () => void
+ [[data]]: string
+ [8]: Victim
+ [...kills]
+ }
+ `,
+ options: [
+ {
+ type: SortType['line-length'],
+ order: SortOrder.desc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ type ParanoiaAgent = {
+ [...kills]
+ [[data]]: string
+ goldenBatAttack(): void
+ [8]: Victim
+ hide?: () => void
+ }
+ `,
+ output: dedent`
+ type ParanoiaAgent = {
+ goldenBatAttack(): void
+ hide?: () => void
+ [[data]]: string
+ [8]: Victim
+ [...kills]
+ }
+ `,
+ options: [
+ {
+ type: SortType['line-length'],
+ order: SortOrder.desc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: '[...kills]',
+ second: '[[data]]',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: '[[data]]',
+ second: 'goldenBatAttack(): void',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectTypesOrder',
+ data: {
+ first: '8',
+ second: 'hide',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+ })
+})