Skip to main content

prefer-type-fest-require-exactly-one

Require TypeFest RequireExactlyOne<T, Keys> over imported aliases like OneOf or RequireOnlyOne.

Targeted pattern scopeโ€‹

This rule reports imported OneOf/RequireOnlyOne aliases and prefers RequireExactlyOne<T, Keys> for XOR-style object constraints.

Use this when callers must choose one mode, not multiple modes (for example, id or slug, apiKey or token).

What this rule reportsโ€‹

  • Type references that resolve to imported OneOf aliases.
  • Type references that resolve to imported RequireOnlyOne aliases.

Detection boundariesโ€‹

  • โœ… Reports imported aliases with direct named imports.
  • โŒ Does not report namespace-qualified alias usage.
  • โœ… Auto-fixes imported alias references to RequireExactlyOne when replacement is syntactically safe.
  • โœ… Alias coverage is configurable with enforcedAliasNames.

Why this rule existsโ€‹

RequireExactlyOne is the canonical TypeFest utility for enforcing exactly one active key among a set. Using the canonical name reduces semantic drift between utility libraries.

This is one of the most error-prone constraints in hand-written unions. Using a known utility keeps intent obvious and consistent.

โŒ Incorrectโ€‹

import type { OneOf } from "type-aliases";

type Auth = OneOf<{
token?: string;
apiKey?: string;
}>;

โœ… Correctโ€‹

import type { RequireExactlyOne } from "type-fest";

type Auth = RequireExactlyOne<{
token?: string;
apiKey?: string;
}>;

Behavior and migration notesโ€‹

  • RequireExactlyOne<T, Keys> encodes XOR object modes where one and only one key can be active.
  • This rule targets alias names with matching semantics (OneOf, RequireOnlyOne).
  • Keep the participating key set small and explicit to avoid hard-to-read error messages in consuming code.

Optionsโ€‹

This rule accepts a single options object:

type PreferTypeFestRequireExactlyOneOptions = {
/**
* Legacy alias names that this rule will report and replace.
*
* @default ["OneOf", "RequireOnlyOne"]
*/
enforcedAliasNames?: ("OneOf" | "RequireOnlyOne")[];
};

Default configuration:

{
enforcedAliasNames: ["OneOf", "RequireOnlyOne"],
}

Flat config setup (default behavior):

import typefest from "eslint-plugin-typefest";

export default [
{
plugins: { typefest },
rules: {
"typefest/prefer-type-fest-require-exactly-one": [
"error",
{ enforcedAliasNames: ["OneOf", "RequireOnlyOne"] },
],
},
},
];

enforcedAliasNames: ["OneOf", "RequireOnlyOne"] (default)โ€‹

Reports both legacy aliases.

enforcedAliasNames: ["RequireOnlyOne"]โ€‹

Reports only RequireOnlyOne and ignores OneOf:

import typefest from "eslint-plugin-typefest";

export default [
{
plugins: { typefest },
rules: {
"typefest/prefer-type-fest-require-exactly-one": [
"error",
{ enforcedAliasNames: ["RequireOnlyOne"] },
],
},
},
];
import type { OneOf, RequireOnlyOne } from "type-aliases";

type A = OneOf<{ a?: string; b?: number }>; // โœ… Not reported
type B = RequireOnlyOne<{ a?: string; b?: number }>; // โŒ Reported

Additional examplesโ€‹

โŒ Incorrect โ€” Additional exampleโ€‹

import type { RequireOnlyOne } from "custom-type-utils";

type LookupInput = RequireOnlyOne<
{
id?: string;
slug?: string;
externalRef?: string;
},
"id" | "slug" | "externalRef"
>;

โœ… Correct โ€” Additional exampleโ€‹

import type { RequireExactlyOne } from "type-fest";

type LookupInput = RequireExactlyOne<
{
id?: string;
slug?: string;
externalRef?: string;
},
"id" | "slug" | "externalRef"
>;

โœ… Correct โ€” Repository-wide usageโ€‹

type AuthInput = RequireExactlyOne<
{ token?: string; apiKey?: string; oauthCode?: string },
"token" | "apiKey" | "oauthCode"
>;

ESLint flat config exampleโ€‹

import typefest from "eslint-plugin-typefest";

export default [
{
plugins: { typefest },
rules: {
"typefest/prefer-type-fest-require-exactly-one": "error",
},
},
];

When not to use itโ€‹

Disable this rule if compatibility requirements force existing alias names.

Package documentationโ€‹

TypeFest package documentation:

Source file: source/require-exactly-one.d.ts

/**
Create a type that requires exactly one of the given keys and disallows more. The remaining keys are kept as is.

Use-cases:
- Creating interfaces for components that only need one of the keys to display properly.
- Declaring generic keys in a single place for a single use-case that gets narrowed down via `RequireExactlyOne`.

The caveat with `RequireExactlyOne` is that TypeScript doesn't always know at compile time every key that will exist at runtime. Therefore `RequireExactlyOne` can't do anything to prevent extra keys it doesn't know about.

@example
```
import type {RequireExactlyOne} from 'type-fest';

type Responder = {
text: () => string;
json: () => string;
secure: boolean;
};

const responder: RequireExactlyOne<Responder, 'text' | 'json'> = {
// Adding a `text` key here would cause a compile error.

json: () => '{"message": "ok"}',
secure: true,
};
```

@category Object
*/

Rule catalog ID: R058

Further readingโ€‹

Adoption resourcesโ€‹