eslint-plugin-functional

repository·main·Indexed 21 days ago

https://github.com/eslint-functional/eslint-plugin-functional

An ESLint plugin that promotes functional programming in JavaScript and TypeScript by disabling mutation and enforcing paradigms such as currying, expression-based logic, and immutability. It provides several configuration presets including Strict, Recommended, and Lite, as well as categorized rulesets for currying, exceptions, mutations, and non-functional paradigms.

Tokens
26.7K
Snippets
85
Records
120
Agent score
75%

What's inside eslint-plugin-functional

  1. Overview of eslint-plugin-functional

    main
    eslint-plugin-functional is an ESLint plugin designed to disable mutation and promote functional programming principles in JavaScript and TypeScript codebases. It provides various rulesets to enforce functional paradigms such as currying, avoiding exceptions, preventing mutations, and restricting non-functional programming styles.
  2. Overview of eslint-plugin-functional rulesets

    main

    The plugin organizes its rules into several thematic categories to enforce functional programming paradigms. You can enable these rules by using the corresponding configuration presets in your ESLint configuration:

    • Currying: Rules related to functional parameter patterns.
    • No Exceptions: Rules to disallow throwing exceptions and try-catch patterns.
    • No Mutations: Rules to enforce data immutability and disallow mutable variables.
    • No Other Paradigms: Rules to restrict object-oriented patterns like classes and this access.
    • No Statements: Rules to disallow imperative control flow like loops and conditional statements.
    • Stylistic: Rules for functional code style preferences.
  3. Use `overrides` to target specific types or libraries

    main

    The overrides option allows you to apply different settings based on where a type is declared (file, library, or package). This is useful for relaxing rules for 3rd party libraries. Only the first matching override is used.

    Each override can:

    • match: Define a specifier (file path, package name, or pattern).
    • options: Provide new configuration options.
    • inherit: Determine if root options should be merged (defaults to true).
    • disable: Completely disable the rule for the matching node if true.
  4. Avoid class inheritance in favor of composition

    main

    When using functional/no-class-inheritance, you should avoid using the extends keyword. Instead, use composition by nesting instances of other classes within your class to share behavior or data.

    /* eslint functional/no-class-inheritance: "error" */
    
    // ✅ Correct: Using composition instead of inheritance
    class Animal {
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
    }
    
    class Dog {
      constructor(name, age) {
        this.animal = new Animal(name, age);
      }
    
      get ageInDogYears() {
        return 7 * this.animal.age;
      }
    }
    
    const dogA = new Dog("Jasper", 2);
    console.log(`${dogA.animal.name} is ${dogA.ageInDogYears} in dog years.`);
  5. Allow 'let' in for-loop initializers with allowInForLoopInit

    main

    By default, let is disallowed everywhere. If you set allowInForLoopInit: true, the rule will permit let declarations specifically within the initializer of a standard for loop (e.g., for (let i = 0; ...) {}).

    Note: This does not allow let in for...of or for...in loops; those must still use const.

    /* eslint functional/no-let: ["error", { "allowInForLoopInit": true }] */
    
    // ✅ Correct (allowed with this option)
    for (let i = 0; i < array.length; i++) {}
    
    // ❌ Incorrect (still disallowed in for...of)
    for (let element of array) {}
  6. Choose an enforcement level for `prefer-immutable-types`

    main

    The enforcement option determines how strictly immutability is applied:

    • None: No immutability is enforced.
    • ReadonlyShallow: Enforces that data is shallowly immutable (e.g., using ReadonlyArray or Readonly<{ prop: string }>).
    • ReadonlyDeep: Enforces deep immutability, though methods on the objects may not be restricted.
    • Immutable: Enforces total deep immutability; nothing can be modified.

    Examples

    Immutable Enforcement ReadonlyArray, ReadonlySet, and ReadonlyMap are not considered fully immutable because they have mutating methods. For Immutable enforcement, you must wrap them in Readonly:

    // ❌ Incorrect (under 'Immutable')
    function array(arg: ReadonlyArray<string>) {}
    
    // ✅ Correct (under 'Immutable')
    function array(arg: Readonly<ReadonlyArray<string>>) {}

    ReadonlyShallow Enforcement Under ReadonlyShallow, standard readonly collections are sufficient:

    // ✅ Correct (under 'ReadonlyShallow')
    function array(arg: ReadonlyArray<{ foo: string }>) {}
    /* eslint functional/prefer-immutable-types: ["error", { "enforcement": "Immutable" }] */
    function set(arg: Readonly<ReadonlySet<string>>) {}
  7. How to type parameters for functional/prefer-immutable-types

    main

    To satisfy this rule, all properties of objects, elements of arrays/tuples, and function signatures must be marked as readonly or use immutable equivalents like ReadonlyArray.

    Arrays and Tuples

    • Incorrect: arg: string[] or arg: [string, number]
    • Correct: arg: ReadonlyArray<string> or arg: readonly [string, number]

    Objects

    • Incorrect: arg: { prop: string } or arg: { readonly prop: string; prop2: string }
    • Correct: arg: { readonly prop: string; readonly prop2: string } or arg: Readonly<T>

    Function Types

    The rule also checks function signatures within interfaces and type aliases.

    • Incorrect: (arg: string[]) => void
    • Correct: (arg: ReadonlyArray<string>) => void

    Primitives and Special Types

    Primitives (string, number, boolean, etc.), enums, symbols, and unknown/any/never are considered immutable by default and do not require special handling.

    // Correct usage examples
    function array1(arg: ReadonlyArray<string>) {}
    function array2(arg: ReadonlyArray<ReadonlyArray<string>>) {}
    function array3(arg: readonly [string, number]) {}
    
    function object1(arg: { readonly prop: string }) {}
    function object2(arg: { readonly prop: string; readonly prop2: string }) {}
    
    interface Foo1 {
      (arg: ReadonlyArray<string>): void;
    }
    
    type Foo3 = (arg: ReadonlyArray<string>) => void;
  8. Apply type-based overrides with overrides

    main

    The overrides option allows you to apply different rule settings based on the function's type. This is particularly useful for handling 3rd party library types that might not follow your strict functional rules.

    Note: This requires type information.

    Each override object contains:

    • match: An array of specifiers to match the function type against (can match file, lib, or package).
    • options: The configuration to apply when a match is found.
    • inherit: Whether to inherit the root options (defaults to true).
    • disable: If true, the rule is completely disabled for matching nodes.

    Only the first matching override will be used.

  9. Use ifExhaustive to mimic do expressions

    main

    Setting allowReturningBranches: "ifExhaustive" allows conditional statements if they are exhaustive (every case is covered and returns a value). This allows you to use a switch or if/else block as an expression by wrapping it in an IIFE.

    Note: This option is currently incompatible with the no-else-return rule; else statements must contain a return statement.

    const x = (() => {
      switch (y) {
        case "a":
          return 1;
        case "b":
          return 2;
        default:
          return 0;
      }
    })();
  10. How functional/functional-parameters works

    main

    In functional programming, parameters should be known and explicit. This rule prevents patterns that allow an unknown number of arguments, which makes currying difficult or impossible.

    Disallowed Patterns

    • Using the arguments keyword to access passed arguments.
    • Using rest parameters (...args) to capture an arbitrary number of arguments.

    Allowed Pattern

    • Passing an explicit collection (like an array) as a single parameter.

    Example

    Incorrect:

    /* eslint functional/functional-parameters: "error" */
    function add() {
      return arguments.reduce((sum, number) => sum + number, 0);
    }
    
    function add(...numbers) {
      return numbers.reduce((sum, number) => sum + number, 0);
    }

    Correct:

    /* eslint functional/functional-parameters: "error" */
    function add(numbers) {
      return numbers.reduce((sum, number) => sum + number, 0);
    }
  11. Best practices for pure functions and return types

    main

    While functional/prefer-immutable-types focuses on parameters, it is important to consider how parameters affect return types in pure functions.

    If a function takes a Readonly<T> parameter and returns an object containing that parameter, the return type will be constrained by that immutability. To avoid unnecessarily constraining the return type of a function, use generics to capture the specific type of the input.

    Recommended Pattern: Instead of forcing a specific immutable type on the parameter, use a generic constraint to allow the caller to pass in their own type (whether mutable or immutable) while still ensuring the function treats it as immutable internally.

    type Foo = { hello: number };
    
    // ❌ Avoid: This forces the return type to always have an immutable 'foo'
    function addBar(foo: Readonly<Foo>) {
      return {
        foo,
        bar: { world: 2 },
      };
    }
    
    // ✅ Better: Uses generics to preserve the caller's type information
    function addBar<F extends Readonly<Foo>>(foo: F) {
      return {
        foo,
        bar: { world: 2 },
      };
    }
  12. Use `ignoreInferredTypes` to handle external dependencies

    main

    By default (false), the rule flags all types that are not explicitly annotated. If an external dependency uses mutable types in its callbacks, you can set ignoreInferredTypes: true to allow the rule to skip values where the type is inferred by TypeScript, avoiding the need to manually annotate parameters that match external interfaces.

    /* eslint functional/prefer-immutable-types: ["error", { "ignoreInferredTypes": true }] */
    import { acceptsCallback } from "external-dependency";
    
    // The 'options' type is inferred, so it is ignored
    acceptsCallback((options) => {});