Skip to main content

prefer-type-fest-if

Require TypeFest If + Is* utilities over deprecated aliases like IfAny, IfNever, IfUnknown, IfNull, and IfEmptyObject.

Targeted pattern scope​

This rule reports deprecated IfAny/IfNever/IfUnknown/IfNull/IfEmptyObject aliases and migrates usage to canonical If<Is*> patterns from type-fest.

It is intentionally strict about naming consistency and intentionally conservative about fixing. Rewriting IfAny<T, A, B> into If<IsAny<T>, A, B> is a structural transform, not a safe one-token rename.

What this rule reports​

  • Imported type aliases used as identifier type references:
    • IfAny
    • IfNever
    • IfUnknown
    • IfNull
    • IfEmptyObject

Detection boundaries​

  • βœ… Reports import type { IfAny } ... followed by IfAny<...> usage.
  • ❌ Does not report locally renamed imports (import type { IfAny as AliasIfAny } ...).
  • ❌ Does not report namespace-qualified references like TypeUtils.IfAny<...> (the matcher targets identifier references).
  • ❌ Does not auto-fix because replacement requires rebuilding type arguments.

Why this rule exists​

These aliases are deprecated in TypeFest. The canonical pattern is to use If<...> with the corresponding Is* utility (for example, If<IsAny<T>, ...>).

In practice, teams that keep old aliases around end up with mixed style across packages (IfAny, IfUnknown, custom wrappers). This rule prevents that drift.

❌ Incorrect​

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

type Result = IfAny<T, "any", "not-any">;

βœ… Correct​

import type { If, IsAny } from "type-fest";

type Result = If<IsAny<T>, "any", "not-any">;

Behavior and migration notes​

  • Deprecated aliases map to If<Is*> forms (IfAny β†’ If<IsAny<...>>, etc.).
  • This rule intentionally does not auto-fix because argument restructuring must be explicit and reviewable.
  • Keep migration focused on parity: preserve branch types and generic parameter order during conversion.

Additional examples​

❌ Incorrect β€” Additional example​

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

type ParseMode<T> = IfUnknown<T, "strict", "lenient">;

βœ… Correct β€” Additional example​

import type { If, IsUnknown } from "type-fest";

type ParseMode<T> = If<IsUnknown<T>, "strict", "lenient">;

βœ… Correct β€” Repository-wide usage​

import type { If, IsNull } from "type-fest";

type NullLabel<T> = If<IsNull<T>, "nullable", "non-nullable">;

ESLint flat config example​

import typefest from "eslint-plugin-typefest";

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

When not to use it​

Disable this rule if legacy alias naming must be preserved for compatibility.

Package documentation​

TypeFest package documentation:

Source file: source/if.d.ts

/**
* An if-else-like type that resolves depending on whether the given `boolean`
* type is `true` or `false`.
*
* Use-cases:
*
* - You can use this in combination with `Is*` types to create an if-else-like
* experience. For example, `If<IsAny<any>, 'is any', 'not any'>`.
*
* Note:
*
* - Returns a union of if branch and else branch if the given type is `boolean`
* or `any`. For example, `If<boolean, 'Y', 'N'>` will return `'Y' | 'N'`.
* - Returns the else branch if the given type is `never`. For example, `If<never,
* 'Y', 'N'>` will return `'N'`.
*
* @category Type Guard
* @category Utilities
*
* @example
* ```
* import type {If} from 'type-fest';
*
* type A = If<true, 'yes', 'no'>;
* //=> 'yes'
*
* type B = If<false, 'yes', 'no'>;
* //=> 'no'
*
* type C = If<boolean, 'yes', 'no'>;
* //=> 'yes' | 'no'
*
* type D = If<any, 'yes', 'no'>;
* //=> 'yes' | 'no'
*
* type E = If<never, 'yes', 'no'>;
* //=> 'no'
* ```;
*
* @example
* ```
* import type {If, IsAny, IsNever} from 'type-fest';
*
* type A = If<IsAny<unknown>, 'is any', 'not any'>;
* //=> 'not any'
*
* type B = If<IsNever<never>, 'is never', 'not never'>;
* //=> 'is never'
* ```;
*
* @example
* ```
* import type {If, IsEqual} from 'type-fest';
*
* type IfEqual<T, U, IfBranch, ElseBranch> = If<IsEqual<T, U>, IfBranch, ElseBranch>;
*
* type A = IfEqual<string, string, 'equal', 'not equal'>;
* //=> 'equal'
*
* type B = IfEqual<string, number, 'equal', 'not equal'>;
* //=> 'not equal'
* ```
*
* Note: Sometimes using the `If` type can make an implementation non–tail-recursive, which can impact performance. In such cases, it's better to use a conditional directly. Refer to the following example:
*
* @example
* ```
* import type {If, IsEqual, StringRepeat} from 'type-fest';
*
* type HundredZeroes = StringRepeat<'0', 100>;
*
* // The following implementation is not tail recursive
* type Includes<S extends string, Char extends string> =
* S extends `${infer First}${infer Rest}`
* ? If<IsEqual<First, Char>,
* 'found',
* Includes<Rest, Char>>
* : 'not found';
*
* // Hence, instantiations with long strings will fail
* // @ts-expect-error
* type Fails = Includes<HundredZeroes, '1'>;
* // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* // Error: Type instantiation is excessively deep and possibly infinite.
*
* // However, if we use a simple conditional instead of `If`, the implementation becomes tail-recursive
* type IncludesWithoutIf<S extends string, Char extends string> =
* S extends `${infer First}${infer Rest}`
* ? IsEqual<First, Char> extends true
* ? 'found'
* : IncludesWithoutIf<Rest, Char>
* : 'not found';
*
* // Now, instantiations with long strings will work
* type Works = IncludesWithoutIf<HundredZeroes, '1'>;
* //=> 'not found'
* ```
*/

Rule catalog ID: R041

Further reading​

Adoption resources​