Skip to content

Proposal: Add Overloads to Array.prototype.includes and ReadonlyArray.prototype.includes for Improved Type Safety and Usability #61618

Closed
@sugoroku-y

Description

@sugoroku-y

πŸ” Search Terms

ReadonlyArray includes type-guard

βœ… Viability Checklist

⭐ Suggestion

Add an overload to Array.prototype.includes and ReadonlyArray.prototype.includes that narrows the type of the searched value when the array is a literal tuple (e.g., declared with as const) containing only primitive literal values such as strings, numbers, booleans, null, or undefined.

This overload enables stricter type checking for includes calls and allows using includes as a type guard only when it is safe and meaningful to do so, avoiding false assumptions when working with general arrays.

interface ReadonlyArray<T> {
  includes<U>(
    s: U,
    ...args: IsLiteralArray<this> extends true
      ? T extends U
        ? [fromIndex?: number]
        : never
      : never
  ): s is IsLiteralArray<this> extends true ? (T extends U ? T : never) : never;
  includes<U>(
    s: U,
    ...args: T extends U ? [fromIndex?: number] : never
  ): boolean;
}

interface Array<T> {
  includes<U>(
    s: U,
    ...args: T extends U ? [fromIndex?: number] : never
  ): T extends U ? boolean : false;
}

Supporting Utility Types:

type IsUnion<T> = (T extends T ? (p: T) => 0 : never) extends (p: T) => 0
  ? false
  : true;

type IsLiteral<T> = IsUnion<T> extends true
  ? false
  : T extends null | undefined | boolean
  ? true
  : T extends PropertyKey
  ? {} extends Record<T, unknown>
    ? false
    : true
  : T extends bigint
  ? {} extends Record<`${T}`, unknown>
    ? false
    : true
  : false;

type IsLiteralArray<T extends readonly unknown[]> = IsUnion<T> extends true
  ? false
  : IsLiteral<T['length']> extends true
  ? T extends readonly [infer F, ...infer R]
    ? IsLiteral<F> extends true
      ? IsLiteralArray<R>
      : false
    : true
  : false;

You can try it in the TypeScript Playground here

πŸ“ƒ Motivating Example

Currently, code like the following produces a type error, even though it is semantically correct:

const KEYS = ['aaa', 'bbb', 'ccc'] as const;

function handleKey(key: string) {
  if (KEYS.includes(key)) {
    // This should narrow key to 'aaa' | 'bbb' | 'ccc'
    doSomething(key);
  }
}

To avoid this error, users often resort to unsafe casts or unnecessary widening of the array's type. On the other hand, relaxing the parameter type (e.g., using unknown) can lead to incorrect code such as:

const KEYS = ['aaa', 'bbb', 'ccc'] as const;

function handleKey(key: number) {
  if (KEYS.includes(key)) {
    // This is likely a bug, but it won't be caught
  }
}

The proposed overload solves this by enabling proper narrowing only when it is safe (i.e., when the array is a literal tuple of literals), and producing compile-time errors when there is no possible match.

πŸ’» Use Cases

  1. What do you want to use this for?
  2. What shortcomings exist with current approaches?
  3. What workarounds are you using in the meantime?

1. What do you want to use this for?

To safely narrow the type of a variable when checking its inclusion in a fixed set of literal values using .includes, especially in configuration validation, value guards, and API parameter checks.

2. What shortcomings exist with current approaches?

  • includes does not currently act as a type guard even when used with literal tuples.
  • It requires manual guards or unsafe type assertions.
  • Overly permissive typing (e.g., accepting unknown) eliminates type safety and allows obvious mistakes.

3. What workarounds are you using in the meantime?

  • Writing custom type guards (key is 'a' | 'b' | 'c')
  • Using Set.has() instead of arrays
  • Using type assertions like as any
  • Duplicating union types and literal arrays

Metadata

Metadata

Assignees

No one assigned

    Labels

    DeclinedThe issue was declined as something which matches the TypeScript visionSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions