Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/comparisons/src/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -17306,7 +17306,8 @@
"flint": {
"name": "typeConstituentDuplicates",
"plugin": "ts",
"preset": "logical"
"preset": "logical",
"status": "implemented"
},
"oxlint": [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
description: "Reports duplicate types in unions and intersections."
title: "typeConstituentDuplicates"
topic: "rules"
---

import { Tabs, TabItem } from "@astrojs/starlight/components";
import { RuleEquivalents } from "~/components/RuleEquivalents";
import RuleSummary from "~/components/RuleSummary.astro";

<RuleSummary plugin="ts" rule="typeConstituentDuplicates" />

Duplicate types in unions and intersections are redundant and can make code harder to read.

## Examples

### Duplicate Union Types

<Tabs>
<TabItem label="❌ Incorrect">

```ts
type Value = string | number | string;
```

</TabItem>
<TabItem label="✅ Correct">

```ts
type Value = string | number;
```

</TabItem>
</Tabs>

### Duplicate Intersection Types

<Tabs>
<TabItem label="❌ Incorrect">

```ts
type Combined = BaseType & ExtendedType & BaseType;
```

</TabItem>
<TabItem label="✅ Correct">

```ts
type Combined = BaseType & ExtendedType;
```

</TabItem>
</Tabs>

## Options

This rule is not configurable.

## When Not To Use It

If you have generated types or complex type manipulations where duplicates are intentional,
you may want to disable this rule.

## Further Reading

- [TypeScript: Union Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types)
- [TypeScript: Intersection Types](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types)

## Equivalents in Other Linters

<RuleEquivalents pluginId="ts" ruleId="typeConstituentDuplicates" />
2 changes: 2 additions & 0 deletions packages/ts/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ import sequences from "./rules/sequences.ts";
import shadowedRestrictedNames from "./rules/shadowedRestrictedNames.ts";
import sparseArrays from "./rules/sparseArrays.ts";
import symbolDescriptions from "./rules/symbolDescriptions.ts";
import typeConstituentDuplicates from "./rules/typeConstituentDuplicates.ts";
import typeofComparisons from "./rules/typeofComparisons.ts";
import unassignedVariables from "./rules/unassignedVariables.ts";
import undefinedVariables from "./rules/undefinedVariables.ts";
Expand Down Expand Up @@ -465,6 +466,7 @@ export const ts = createPlugin({
shadowedRestrictedNames,
sparseArrays,
symbolDescriptions,
typeConstituentDuplicates,
typeofComparisons,
unassignedVariables,
undefinedVariables,
Expand Down
52 changes: 52 additions & 0 deletions packages/ts/src/rules/typeConstituentDuplicates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import rule from "./typeConstituentDuplicates.ts";
import { ruleTester } from "./ruleTester.ts";

Check failure on line 2 in packages/ts/src/rules/typeConstituentDuplicates.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Expected "./ruleTester.ts" to come before "./typeConstituentDuplicates.ts"

ruleTester.describe(rule, {
invalid: [
{
code: `type Value = string | string;`,
snapshot: `type Value = string | string;
~~~~~~
Duplicate type in union.`,
},
{
code: `type Value = 1 | 2 | 1;`,
snapshot: `type Value = 1 | 2 | 1;
~
Duplicate type in union.`,
},
{
code: `type Value = "a" | "b" | "a";`,
snapshot: `type Value = "a" | "b" | "a";
~~~
Duplicate type in union.`,
},
{
code: `type Value = A & B & A;`,
snapshot: `type Value = A & B & A;
~
Duplicate type in intersection.`,
},
{
code: `type Value = { name: string } & { name: string };`,
snapshot: `type Value = { name: string } & { name: string };
~~~~~~~~~~~~~~~~
Duplicate type in intersection.`,
},
{
code: `type Value = Array<string> | Array<string>;`,
snapshot: `type Value = Array<string> | Array<string>;
~~~~~~~~~~~~~
Duplicate type in union.`,
},
],
valid: [
`type Value = string | number;`,
`type Value = 1 | 2 | 3;`,
`type Value = "a" | "b" | "c";`,
`type Value = A & B & C;`,
`type Value = { name: string } & { age: number };`,
`type Value = Array<string> | Array<number>;`,
`type Value = string;`,
],
});
60 changes: 60 additions & 0 deletions packages/ts/src/rules/typeConstituentDuplicates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { typescriptLanguage } from "@flint.fyi/typescript-language";
import type ts from "typescript";

import { ruleCreator } from "./ruleCreator.ts";

export default ruleCreator.createRule(typescriptLanguage, {
about: {
description: "Reports duplicate types in unions and intersections.",
id: "typeConstituentDuplicates",
presets: ["logical"],
},
messages: {
duplicateIntersection: {
primary: "Duplicate type in intersection.",
secondary: ["This type appears more than once in the intersection."],
suggestions: ["Remove the duplicate type."],
},
duplicateUnion: {
primary: "Duplicate type in union.",
secondary: ["This type appears more than once in the union."],
suggestions: ["Remove the duplicate type."],
},
},
setup(context) {
function checkDuplicates(
types: ts.NodeArray<ts.TypeNode>,
sourceFile: ts.SourceFile,
messageId: "duplicateIntersection" | "duplicateUnion",
) {
const seen = new Map<string, ts.TypeNode>();

for (const typeNode of types) {
const text = typeNode.getText(sourceFile);

if (seen.has(text)) {
context.report({
message: messageId,
range: {
begin: typeNode.getStart(sourceFile),
end: typeNode.getEnd(),
},
});
} else {
seen.set(text, typeNode);
}
}
}

return {
visitors: {
IntersectionType(node, { sourceFile }) {
checkDuplicates(node.types, sourceFile, "duplicateIntersection");
},
UnionType(node, { sourceFile }) {
checkDuplicates(node.types, sourceFile, "duplicateUnion");
},
},
};
},
});
Loading